From f46105afa1062d1bae8f783c5734d9d8129385a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E9=9F=AC=20=E5=BC=A0?= Date: Fri, 5 Feb 2021 16:44:33 +0800 Subject: [PATCH 1/2] Full async query model --- Dictionary.csproj | 3 +- src/Dictionary.cs | 126 ++++++++++++++++++++++++++++------------------ src/ECDict.cs | 26 ++++++---- src/Settings.cs | 8 +-- src/Synonyms.cs | 26 ++++++---- src/iciba.cs | 18 +++---- 6 files changed, 124 insertions(+), 83 deletions(-) diff --git a/Dictionary.csproj b/Dictionary.csproj index b6972d0..e3fed7f 100644 --- a/Dictionary.csproj +++ b/Dictionary.csproj @@ -36,11 +36,12 @@ - + + diff --git a/src/Dictionary.cs b/src/Dictionary.cs index 527a010..4998835 100644 --- a/src/Dictionary.cs +++ b/src/Dictionary.cs @@ -1,12 +1,14 @@ -using Newtonsoft.Json; -using System; +using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; using System.Runtime.InteropServices; //using System.Speech.Synthesis; using System.Text; +using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; @@ -14,13 +16,13 @@ namespace Dictionary { - public class Main : IPlugin, ISettingProvider, IResultUpdated + public class Main : IAsyncPlugin, ISettingProvider, IResultUpdated { private ECDict ecdict; private WordCorrection wordCorrection; private Synonyms synonyms; private Iciba iciba; - private PluginInitContext context; + internal static PluginInitContext Context { get; private set; } private Settings settings; //private SpeechSynthesizer synth; @@ -35,7 +37,7 @@ public Control CreateSettingPanel() return new DictionarySettings(settings); } - public void Init(PluginInitContext context) + public async Task InitAsync(PluginInitContext context) { string CurrentPath = context.CurrentPluginMetadata.PluginDirectory; @@ -44,7 +46,7 @@ public void Init(PluginInitContext context) string ConfigFile = CurrentPath + "/config/config.json"; if (File.Exists(ConfigFile)) - settings = JsonConvert.DeserializeObject(File.ReadAllText(ConfigFile)); + settings = await JsonSerializer.DeserializeAsync(File.OpenRead(ConfigFile)).ConfigureAwait(false); else settings = new Settings(); settings.ConfigFile = ConfigFile; @@ -53,7 +55,7 @@ public void Init(PluginInitContext context) wordCorrection = new WordCorrection(CurrentPath + "/dicts/frequency_dictionary_en_82_765.txt", settings.MaxEditDistance); synonyms = new Synonyms(settings.BighugelabsToken); iciba = new Iciba(settings.ICIBAToken); - this.context = context; + Context = context; } Result MakeResultItem(string title, string subtitle, string extraAction = null, string word = null) @@ -69,7 +71,7 @@ bool CopyIfNeeded(ActionContext e) } catch (ExternalException ee) { - context.API.ShowMsg("Copy failed, please try later", ee.Message); + Context.API.ShowMsg("Copy failed, please try later", ee.Message); } return true; } @@ -96,7 +98,7 @@ bool ReadWordIfNeeded(ActionContext e) { if (CopyIfNeeded(e)) return true; //if (ReadWordIfNeeded(e)) return false; - context.API.ChangeQuery(ActionWord + " " + (word ?? QueryWord) + extraAction); + Context.API.ChangeQuery(ActionWord + " " + (word ?? QueryWord) + extraAction); return false; }; } @@ -124,38 +126,70 @@ private Result MakeWordResult(Word word) => (settings.ShowEnglishDefinition ? word.definition.Replace("\n", "; ") : word.translation.Replace("\n", "; ")), "!", word.word); + private class WordEqualityComparer : IEqualityComparer + { + public static WordEqualityComparer instance = new WordEqualityComparer(); + + public bool Equals([AllowNull] Result x, [AllowNull] Result y) + { + if (x.Equals(y)) + return true; + else + return x.Title == y.Title; + + } + + public int GetHashCode([DisallowNull] Result obj) + { + return obj.Title.GetHashCode(); + } + } + // First-level query. // English -> Chinese, supports fuzzy search. - private List FirstLevelQuery(Query query) + private async Task> FirstLevelQueryAsync(Query query, CancellationToken token) { string queryWord = query.Search; - IEnumerable results = Enumerable.Empty(); + HashSet results = new HashSet(WordEqualityComparer.instance); // Pull fully match first. - Word fullMatch = ecdict.Query(query.Search); + Word fullMatch = await ecdict.QueryAsync(query.Search, token).ConfigureAwait(false); if (fullMatch != null) - results = results.Append(fullMatch); + results.Add(MakeWordResult(fullMatch)); + + token.ThrowIfCancellationRequested(); + ResultsUpdated?.Invoke(this, new ResultUpdatedEventArgs { Results = results.ToList(), Query = query }); // Then fuzzy search results. (since it's usually only a few) List suggestions = wordCorrection.Correct(queryWord); + token.ThrowIfCancellationRequested(); - return results.Concat(ecdict.QueryRange(suggestions)) - .Concat(ecdict.QueryBeginningWith(queryWord)) - .Distinct() - .Select(w => MakeWordResult(w)) - .ToList(); + await foreach (var word in ecdict.QueryRange(suggestions.Select(x => x.term), token).Select(w => MakeWordResult(w)).ConfigureAwait(false)) + { + results.Add(word); + } + + token.ThrowIfCancellationRequested(); + ResultsUpdated?.Invoke(this, new ResultUpdatedEventArgs { Results = results.ToList(), Query = query }); + + await foreach (var word in ecdict.QueryBeginningWith(queryWord, token).Select(w => MakeWordResult(w)).ConfigureAwait(false)) + { + results.Add(word); + } + + return results.ToList(); } // Detailed information of a word. // English -> Phonetic, Translation, Definition, Exchanges, Synonym // Fuzzy search disabled. - private List DetailedQuery(Query query) + private async Task> DetailedQueryAsync(Query query, CancellationToken token) { string queryWord = query.Search[0..^1]; // Remove the ! List results = new List(); - var word = ecdict.Query(queryWord); + var word = await ecdict.QueryAsync(queryWord, token).ConfigureAwait(false); if (word.phonetic != "") results.Add(MakeResultItem(word.phonetic, "Phonetic")); @@ -166,13 +200,14 @@ private List DetailedQuery(Query query) if (word.exchange != "") results.Add(MakeResultItem("Exchanges", word.exchange, "e")); + token.ThrowIfCancellationRequested(); ResultsUpdated?.Invoke(this, new ResultUpdatedEventArgs { Query = query, Results = results }); - var synonymsResult = string.Join("; ", synonyms.Query(word.word)); + var synonymsResult = string.Join("; ", await synonyms.QueryAsync(word.word, token).ConfigureAwait(false)); if (synonymsResult != "") results.Add(MakeResultItem("Synonym", synonymsResult, "s")); @@ -183,13 +218,13 @@ private List DetailedQuery(Query query) // Translations of a word. // English -> Translations // Fuzzy search disabled. - private List TranslationQuery(Query query) + private async Task> TranslationQueryAsync(Query query, CancellationToken token) { string queryWord = query.Search[0..^2]; // Get the word List results = new List(); - var word = ecdict.Query(queryWord); + var word = await ecdict.QueryAsync(queryWord, token).ConfigureAwait(false); foreach (var translation in word.translation.Split('\n')) { @@ -202,13 +237,13 @@ private List TranslationQuery(Query query) // Definitions of a word. // English -> Definitions // Fuzzy search disabled. - private List DefinitionQuery(Query query) + private async Task> DefinitionQueryAsync(Query query, CancellationToken token) { string queryWord = query.Search[0..^2]; // Get the word List results = new List(); - var word = ecdict.Query(queryWord); + var word = await ecdict.QueryAsync(queryWord, token).ConfigureAwait(false); foreach (var definition in word.definition.Split('\n')) { @@ -221,13 +256,13 @@ private List DefinitionQuery(Query query) // Exchanges of a word. // English -> Exchanges // Fuzzy search disabled. - private List ExchangeQuery(Query query) + private async Task> ExchangeQueryAsync(Query query, CancellationToken token) { string queryWord = query.Search[0..^2]; // Get the word List results = new List(); - var word = ecdict.Query(queryWord); + var word = await ecdict.QueryAsync(queryWord, token); foreach (var exchange in word.exchange.Split('/')) { @@ -241,33 +276,28 @@ private List ExchangeQuery(Query query) // English -> Synonyms // Fuzzy search disabled. // Internet access needed. - private List SynonymQuery(Query query) + private async Task> SynonymQueryAsync(Query query, CancellationToken token) { string queryWord = query.Search[0..^2]; // Get the word List results = new List(); - var syns = synonyms.Query(queryWord); - - foreach (var syn in syns) - { - results.Add(MakeWordResult(ecdict.Query(syn))); - } + var syns = await synonyms.QueryAsync(queryWord, token).ConfigureAwait(false); - return results; + return await ecdict.QueryRange(syns, token).Select(w => MakeWordResult(w)).ToListAsync(token); } // Chinese translation of a word. // English -> Synonyms // Fuzzy search disabled. // Internet access needed. - private List ChineseQuery(Query query) + private async Task> ChineseQueryAsync(Query query, CancellationToken token) { string queryWord = query.Search; // Get the word List results = new List(); - var translations = iciba.Query(queryWord); + var translations = await iciba.QueryAsync(queryWord, token).ConfigureAwait(false); if (translations.Count == 0) { @@ -295,7 +325,7 @@ private bool IsChinese(string cn) return false; } - public List Query(Query query) + public async Task> QueryAsync(Query query, CancellationToken token) { ActionWord = query.ActionKeyword; string queryWord = query.Search; @@ -303,18 +333,18 @@ public List Query(Query query) QueryWord = queryWord; if (queryWord.Length < 2) - return FirstLevelQuery(query); + return await FirstLevelQueryAsync(query, token).ConfigureAwait(false); - return queryWord[^2..] switch + return await (queryWord[^2..] switch { - "!d" => DefinitionQuery(query), - "!t" => TranslationQuery(query), - "!e" => ExchangeQuery(query), - "!s" => SynonymQuery(query), - _ when queryWord[^1] == '!' => DetailedQuery(query), - _ when IsChinese(queryWord) => ChineseQuery(query), - _ => FirstLevelQuery(query) - }; + "!d" => DefinitionQueryAsync(query, token), + "!t" => TranslationQueryAsync(query, token), + "!e" => ExchangeQueryAsync(query, token), + "!s" => SynonymQueryAsync(query, token), + _ when queryWord[^1] == '!' => DetailedQueryAsync(query, token), + _ when IsChinese(queryWord) => ChineseQueryAsync(query, token), + _ => FirstLevelQueryAsync(query, token) + }).ConfigureAwait(false); } } } diff --git a/src/ECDict.cs b/src/ECDict.cs index 62255f2..507baea 100644 --- a/src/ECDict.cs +++ b/src/ECDict.cs @@ -3,7 +3,9 @@ using System.Data; using System.Data.SQLite; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace Dictionary @@ -19,7 +21,7 @@ public ECDict(string filename) // This will only return exact match. // Return null if not found. - public Word Query(string word) + public async Task QueryAsync(string word, CancellationToken token) { if (word == "") return null; @@ -28,7 +30,7 @@ public Word Query(string word) Word ret = null; using SQLiteCommand cmd = new SQLiteCommand(sql, conn); - using SQLiteDataReader reader = cmd.ExecuteReader(); + using SQLiteDataReader reader = await cmd.ExecuteReaderAsync(token).ConfigureAwait(false) as SQLiteDataReader; if (reader.Read()) ret = new Word(reader); @@ -36,9 +38,9 @@ public Word Query(string word) return ret; } - public IEnumerable QueryRange(IEnumerable words) + public async IAsyncEnumerable QueryRange(IEnumerable words, [EnumeratorCancellation] CancellationToken token) { - string queryTerms = string.Join(',', words.Select(w => $"'{w.term}'")); + string queryTerms = string.Join(',', words); if (queryTerms.Length == 0) yield break; @@ -46,15 +48,16 @@ public IEnumerable QueryRange(IEnumerable words) using SQLiteCommand cmd = new SQLiteCommand(sql, conn); - using SQLiteDataReader reader = cmd.ExecuteReader(); + using SQLiteDataReader reader = await cmd.ExecuteReaderAsync(token).ConfigureAwait(false) as SQLiteDataReader; - while (reader.Read()) + while (await reader.ReadAsync(token).ConfigureAwait(false)) yield return new Word(reader); + } // This will include exact match and words beginning with it - public IEnumerable QueryBeginningWith(string word, int limit = 20) + public async IAsyncEnumerable QueryBeginningWith(string word, [EnumeratorCancellation] CancellationToken token = default, int limit = 20) { if (word.Length == 0) yield break; @@ -62,11 +65,16 @@ public IEnumerable QueryBeginningWith(string word, int limit = 20) "%' order by frq > 0 desc, frq asc limit " + limit; using SQLiteCommand cmd = new SQLiteCommand(sql, conn); - using SQLiteDataReader reader = cmd.ExecuteReader(); - while (reader.Read()) + using SQLiteDataReader reader = await cmd.ExecuteReaderAsync(token).ConfigureAwait(false) as SQLiteDataReader; + while (await reader.ReadAsync(token).ConfigureAwait(false)) { yield return new Word(reader); } } + + internal Task QueryAsync(string queryWord) + { + throw new NotImplementedException(); + } } } diff --git a/src/Settings.cs b/src/Settings.cs index f30eeb4..b7f6fab 100644 --- a/src/Settings.cs +++ b/src/Settings.cs @@ -1,9 +1,9 @@ -using Newtonsoft.Json; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using System.Text.Json; using System.Threading.Tasks; namespace Dictionary @@ -17,9 +17,9 @@ public class Settings public bool ShowEnglishDefinition = false; public string WordWebsite = ""; - public void Save() + public async void Save() { - File.WriteAllText(ConfigFile, JsonConvert.SerializeObject(this)); + await JsonSerializer.SerializeAsync(File.OpenWrite(ConfigFile), this); } } } diff --git a/src/Synonyms.cs b/src/Synonyms.cs index 65a931f..0cbe944 100644 --- a/src/Synonyms.cs +++ b/src/Synonyms.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace Dictionary @@ -15,23 +16,26 @@ public Synonyms(string apiToken) { ApiToken = apiToken; } - public List Query(string vocab) + public async Task> QueryAsync(string vocab, CancellationToken token) { List ret = new List(); try { - WebRequest request = WebRequest.Create( - string.Format("http://words.bighugelabs.com/api/2/{0}/{1}/", ApiToken, vocab)); - using WebResponse response = request.GetResponse(); - Stream dataStream = response.GetResponseStream(); + var dataStream = await Main.Context.API.HttpGetStreamAsync($"http://words.bighugelabs.com/api/2/{ApiToken}/{vocab}/", token).ConfigureAwait(false); + using StreamReader reader = new StreamReader(dataStream); - - while(!reader.EndOfStream) + + return ParseResult(reader); + + static IEnumerable ParseResult(StreamReader reader) { - var line = reader.ReadLine(); - if (line == "") continue; - var parts = line.Split('|'); - if (parts[1] == "syn") ret.Add(parts[2]); + while (!reader.EndOfStream) + { + var line = reader.ReadLine(); + if (line == "") continue; + var parts = line.Split('|'); + if (parts[1] == "syn") yield return parts[2]; + } } } catch (Exception) { } diff --git a/src/iciba.cs b/src/iciba.cs index d99290b..8fb5d39 100644 --- a/src/iciba.cs +++ b/src/iciba.cs @@ -1,10 +1,11 @@ -using Newtonsoft.Json; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Text; +using System.Text.Json; +using System.Threading; using System.Threading.Tasks; namespace Dictionary @@ -50,18 +51,15 @@ public class ServerResponse public Iciba(string key) { token = key; } // Chinese to English. Internet access needed. - public List Query(string word) + public async Task> QueryAsync(string word, CancellationToken cancelToken) { List ret = new List(); try { - WebRequest request = WebRequest.Create( - String.Format("http://dict-co.iciba.com/api/dictionary.php?w={0}&key={1}&type=json", word, token)); - WebResponse response = request.GetResponse(); - Stream dataStream = response.GetResponseStream(); - StreamReader reader = new StreamReader(dataStream); - //dynamic rsp = JsonConvert.DeserializeObject(reader.ReadToEnd()); - var rsp = JsonConvert.DeserializeObject(reader.ReadToEnd()); + var dataStream = await Main.Context.API.HttpGetStreamAsync($"http://dict-co.iciba.com/api/dictionary.php?w={word}&key={token}&type=json", + cancelToken).ConfigureAwait(false); + + var rsp = await JsonSerializer.DeserializeAsync(dataStream).ConfigureAwait(false); if (rsp.symbols == null) return ret; foreach (var symbol in rsp.symbols) From fedc738d3d2c4fa8a5bdd1a70e0d079125c74feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E9=9F=AC=20=E5=BC=A0?= Date: Fri, 5 Feb 2021 17:43:09 +0800 Subject: [PATCH 2/2] fix an mismatched query --- src/ECDict.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ECDict.cs b/src/ECDict.cs index 507baea..573803f 100644 --- a/src/ECDict.cs +++ b/src/ECDict.cs @@ -15,7 +15,7 @@ class ECDict readonly SQLiteConnection conn; public ECDict(string filename) { - conn = new SQLiteConnection("Data Source=" + filename + ";Version=3;"); + conn = new SQLiteConnection("Data Source=" + filename + ";Version=3;Read Only=True"); conn.Open(); } @@ -40,11 +40,11 @@ public async Task QueryAsync(string word, CancellationToken token) public async IAsyncEnumerable QueryRange(IEnumerable words, [EnumeratorCancellation] CancellationToken token) { - string queryTerms = string.Join(',', words); + string queryTerms = string.Join("','", words); if (queryTerms.Length == 0) yield break; - string sql = $"select * from stardict where word in ({queryTerms})"; + string sql = $"select * from stardict where word in ('{queryTerms}')"; using SQLiteCommand cmd = new SQLiteCommand(sql, conn);