Merge branch 'dev' into code_quality

This commit is contained in:
VictoriousRaptor 2025-07-14 21:23:24 +08:00 committed by GitHub
commit 7cc0d232be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 146 additions and 95 deletions

View file

@ -103,3 +103,4 @@ Reloadable
metadatas metadatas
WMP WMP
VSTHRD VSTHRD
CJK

View file

@ -14,11 +14,8 @@ namespace Flow.Launcher.Infrastructure
{ {
public class PinyinAlphabet : IAlphabet public class PinyinAlphabet : IAlphabet
{ {
private ConcurrentDictionary<string, (string translation, TranslationMapping map)> _pinyinCache = private readonly ConcurrentDictionary<string, (string translation, TranslationMapping map)> _pinyinCache = new();
new();
private readonly Settings _settings; private readonly Settings _settings;
private ReadOnlyDictionary<string, string> currentDoublePinyinTable; private ReadOnlyDictionary<string, string> currentDoublePinyinTable;
public PinyinAlphabet() public PinyinAlphabet()
@ -28,10 +25,21 @@ namespace Flow.Launcher.Infrastructure
_settings.PropertyChanged += (sender, e) => _settings.PropertyChanged += (sender, e) =>
{ {
if (e.PropertyName == nameof(Settings.UseDoublePinyin) || switch (e.PropertyName)
e.PropertyName == nameof(Settings.DoublePinyinSchema))
{ {
Reload(); case nameof (Settings.ShouldUsePinyin):
if (_settings.ShouldUsePinyin)
{
Reload();
}
break;
case nameof(Settings.UseDoublePinyin):
case nameof(Settings.DoublePinyinSchema):
if (_settings.UseDoublePinyin)
{
Reload();
}
break;
} }
}; };
} }
@ -44,105 +52,142 @@ namespace Flow.Launcher.Infrastructure
private void CreateDoublePinyinTableFromStream(Stream jsonStream) private void CreateDoublePinyinTableFromStream(Stream jsonStream)
{ {
Dictionary<string, Dictionary<string, string>> table = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string>>>(jsonStream); var table = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string>>>(jsonStream) ??
string schemaKey = _settings.DoublePinyinSchema.ToString(); // Convert enum to string throw new InvalidOperationException("Failed to deserialize double pinyin table: result is null");
if (!table.TryGetValue(schemaKey, out var value))
var schemaKey = _settings.DoublePinyinSchema.ToString();
if (!table.TryGetValue(schemaKey, out var schemaDict))
{ {
throw new ArgumentException("DoublePinyinSchema is invalid or double pinyin table is broken."); throw new ArgumentException($"DoublePinyinSchema '{schemaKey}' is invalid or double pinyin table is broken.");
} }
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(value);
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(schemaDict);
} }
private void LoadDoublePinyinTable() private void LoadDoublePinyinTable()
{ {
if (_settings.UseDoublePinyin) if (!_settings.UseDoublePinyin)
{ {
var tablePath = Path.Join(AppContext.BaseDirectory, "Resources", "double_pinyin.json"); currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
try return;
{
using var fs = File.OpenRead(tablePath);
CreateDoublePinyinTableFromStream(fs);
}
catch (System.Exception e)
{
Log.Exception(nameof(PinyinAlphabet), "Failed to load double pinyin table from file: " + tablePath, e);
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
}
} }
else
var tablePath = Path.Combine(AppContext.BaseDirectory, "Resources", "double_pinyin.json");
try
{ {
using var fs = File.OpenRead(tablePath);
CreateDoublePinyinTableFromStream(fs);
}
catch (FileNotFoundException e)
{
Log.Exception(nameof(PinyinAlphabet), $"Double pinyin table file not found: {tablePath}", e);
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
}
catch (DirectoryNotFoundException e)
{
Log.Exception(nameof(PinyinAlphabet), $"Directory not found for double pinyin table: {tablePath}", e);
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
}
catch (UnauthorizedAccessException e)
{
Log.Exception(nameof(PinyinAlphabet), $"Access denied to double pinyin table: {tablePath}", e);
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
}
catch (System.Exception e)
{
Log.Exception(nameof(PinyinAlphabet), $"Failed to load double pinyin table from file: {tablePath}", e);
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>()); currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
} }
} }
public bool ShouldTranslate(string stringToTranslate) public bool ShouldTranslate(string stringToTranslate)
{ {
// If a string has Chinese characters, we don't need to translate it to pinyin. // If the query (stringToTranslate) does NOT contain Chinese characters,
return _settings.ShouldUsePinyin && !WordsHelper.HasChinese(stringToTranslate); // we should translate the target string to pinyin for matching
return _settings.ShouldUsePinyin && !ContainsChinese(stringToTranslate);
} }
public (string translation, TranslationMapping map) Translate(string content) public (string translation, TranslationMapping map) Translate(string content)
{ {
if (!_settings.ShouldUsePinyin || !WordsHelper.HasChinese(content)) if (!_settings.ShouldUsePinyin || !ContainsChinese(content))
return (content, null); return (content, null);
return _pinyinCache.TryGetValue(content, out var value) return _pinyinCache.TryGetValue(content, out var cached) ? cached : BuildCacheFromContent(content);
? value
: BuildCacheFromContent(content);
} }
private (string translation, TranslationMapping map) BuildCacheFromContent(string content) private (string translation, TranslationMapping map) BuildCacheFromContent(string content)
{ {
var resultList = WordsHelper.GetPinyinList(content); var resultList = WordsHelper.GetPinyinList(content);
var resultBuilder = new StringBuilder(_settings.UseDoublePinyin ? 3 : 4); // Pre-allocate with estimated capacity
var resultBuilder = new StringBuilder();
var map = new TranslationMapping(); var map = new TranslationMapping();
var previousIsChinese = false; var previousIsChinese = false;
for (var i = 0; i < resultList.Length; i++) for (var i = 0; i < resultList.Length; i++)
{ {
if (content[i] >= 0x3400 && content[i] <= 0x9FD5) if (IsChineseCharacter(content[i]))
{ {
string translated = _settings.UseDoublePinyin ? ToDoublePin(resultList[i]) : resultList[i]; var translated = _settings.UseDoublePinyin ? ToDoublePinyin(resultList[i]) : resultList[i];
if (i > 0) if (i > 0)
{ {
resultBuilder.Append(' '); resultBuilder.Append(' ');
} }
map.AddNewIndex(resultBuilder.Length, translated.Length); map.AddNewIndex(resultBuilder.Length, translated.Length);
resultBuilder.Append(translated); resultBuilder.Append(translated);
previousIsChinese = true; previousIsChinese = true;
} }
else else
{ {
// Add space after Chinese characters before non-Chinese characters
if (previousIsChinese) if (previousIsChinese)
{ {
previousIsChinese = false; previousIsChinese = false;
resultBuilder.Append(' '); resultBuilder.Append(' ');
} }
map.AddNewIndex(resultBuilder.Length, resultList[i].Length); map.AddNewIndex(resultBuilder.Length, resultList[i].Length);
resultBuilder.Append(resultList[i]); resultBuilder.Append(resultList[i]);
} }
} }
map.endConstruct(); map.EndConstruct();
var key = resultBuilder.ToString(); var translation = resultBuilder.ToString();
var result = (translation, map);
return _pinyinCache[content] = (key, map); return _pinyinCache[content] = result;
} }
#region Double Pinyin /// <summary>
/// Optimized Chinese character detection using the comprehensive CJK Unicode ranges
private string ToDoublePin(string fullPinyin) /// </summary>
private static bool ContainsChinese(ReadOnlySpan<char> text)
{ {
if (currentDoublePinyinTable.TryGetValue(fullPinyin, out var doublePinyinValue)) foreach (var c in text)
{ {
return doublePinyinValue; if (IsChineseCharacter(c))
return true;
} }
return fullPinyin; return false;
} }
#endregion /// <summary>
/// Check if a character is a Chinese character using comprehensive Unicode ranges
/// Covers CJK Unified Ideographs, Extension A
/// </summary>
private static bool IsChineseCharacter(char c)
{
return (c >= 0x4E00 && c <= 0x9FFF) || // CJK Unified Ideographs
(c >= 0x3400 && c <= 0x4DBF); // CJK Extension A
}
private string ToDoublePinyin(string fullPinyin)
{
return currentDoublePinyinTable.TryGetValue(fullPinyin, out var doublePinyinValue)
? doublePinyinValue
: fullPinyin;
}
} }
} }

View file

@ -5,31 +5,30 @@ namespace Flow.Launcher.Infrastructure
{ {
public class TranslationMapping public class TranslationMapping
{ {
private bool constructed; private bool _isConstructed;
// Assuming one original item maps to multi translated items // Assuming one original item maps to multi translated items
// list[i] is the last translated index + 1 of original index i // list[i] is the last translated index + 1 of original index i
private readonly List<int> originalToTranslated = new(); private readonly List<int> _originalToTranslated = new();
public void AddNewIndex(int translatedIndex, int length) public void AddNewIndex(int translatedIndex, int length)
{ {
if (constructed) if (_isConstructed)
throw new InvalidOperationException("Mapping shouldn't be changed after constructed"); throw new InvalidOperationException("Mapping shouldn't be changed after construction");
_originalToTranslated.Add(translatedIndex + length);
originalToTranslated.Add(translatedIndex + length);
} }
public int MapToOriginalIndex(int translatedIndex) public int MapToOriginalIndex(int translatedIndex)
{ {
int loc = originalToTranslated.BinarySearch(translatedIndex); var searchResult = _originalToTranslated.BinarySearch(translatedIndex);
return loc >= 0 ? loc : ~loc; return searchResult >= 0 ? searchResult : ~searchResult;
} }
public void endConstruct() public void EndConstruct()
{ {
if (constructed) if (_isConstructed)
throw new InvalidOperationException("Mapping has already been constructed"); throw new InvalidOperationException("Mapping has already been constructed");
constructed = true; _isConstructed = true;
} }
} }
} }

View file

@ -328,7 +328,19 @@ namespace Flow.Launcher.Infrastructure.UserSettings
/// <summary> /// <summary>
/// when false Alphabet static service will always return empty results /// when false Alphabet static service will always return empty results
/// </summary> /// </summary>
public bool ShouldUsePinyin { get; set; } = false; private bool _useAlphabet = true;
public bool ShouldUsePinyin
{
get => _useAlphabet;
set
{
if (_useAlphabet != value)
{
_useAlphabet = value;
OnPropertyChanged();
}
}
}
private bool _useDoublePinyin = false; private bool _useDoublePinyin = false;
public bool UseDoublePinyin public bool UseDoublePinyin

View file

@ -1,4 +1,6 @@
using Flow.Launcher.Infrastructure; using System.Collections.Generic;
using System.Reflection;
using Flow.Launcher.Infrastructure;
using NUnit.Framework; using NUnit.Framework;
using NUnit.Framework.Legacy; using NUnit.Framework.Legacy;
@ -34,22 +36,21 @@ namespace Flow.Launcher.Test
mapping.AddNewIndex(2, 2); mapping.AddNewIndex(2, 2);
mapping.AddNewIndex(5, 3); mapping.AddNewIndex(5, 3);
var result = mapping.MapToOriginalIndex(translatedIndex); var result = mapping.MapToOriginalIndex(translatedIndex);
ClassicAssert.AreEqual(expectedOriginalIndex, result); ClassicAssert.AreEqual(expectedOriginalIndex, result);
} }
private int GetOriginalToTranslatedCount(TranslationMapping mapping) private static int GetOriginalToTranslatedCount(TranslationMapping mapping)
{ {
var field = typeof(TranslationMapping).GetField("originalToTranslated", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); var field = typeof(TranslationMapping).GetField("_originalToTranslated", BindingFlags.NonPublic | BindingFlags.Instance);
var list = (System.Collections.Generic.List<int>)field.GetValue(mapping); var list = (List<int>)field.GetValue(mapping);
return list.Count; return list.Count;
} }
private int GetOriginalToTranslatedAt(TranslationMapping mapping, int index) private static int GetOriginalToTranslatedAt(TranslationMapping mapping, int index)
{ {
var field = typeof(TranslationMapping).GetField("originalToTranslated", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); var field = typeof(TranslationMapping).GetField("_originalToTranslated", BindingFlags.NonPublic | BindingFlags.Instance);
var list = (System.Collections.Generic.List<int>)field.GetValue(mapping); var list = (List<int>)field.GetValue(mapping);
return list[index]; return list[index];
} }
} }

View file

@ -371,44 +371,37 @@
OnContent="{DynamicResource enable}" /> OnContent="{DynamicResource enable}" />
</cc:Card> </cc:Card>
<cc:CardGroup Margin="0 4 0 0"> <cc:Card
<cc:Card Title="{DynamicResource ShouldUsePinyin}"
Title="{DynamicResource ShouldUsePinyin}" Margin="0 4 0 0"
Icon="&#xe98a;" Icon="&#xe98a;"
Sub="{DynamicResource ShouldUsePinyinToolTip}" Sub="{DynamicResource ShouldUsePinyinToolTip}">
Type="First"> <ui:ToggleSwitch
<ui:ToggleSwitch IsOn="{Binding ShouldUsePinyin}"
IsOn="{Binding ShouldUsePinyin}" OffContent="{DynamicResource disable}"
OffContent="{DynamicResource disable}" OnContent="{DynamicResource enable}"
OnContent="{DynamicResource enable}" ToolTip="{DynamicResource ShouldUsePinyinToolTip}" />
ToolTip="{DynamicResource ShouldUsePinyinToolTip}" /> </cc:Card>
</cc:Card>
<cc:Card <cc:ExCard
Visibility="{ext:VisibleWhen {Binding ShouldUsePinyin}, Title="{DynamicResource ShouldUseDoublePinyin}"
IsEqualToBool=True}" Icon="&#xf085;"
Title="{DynamicResource ShouldUseDoublePinyin}" Sub="{DynamicResource ShouldUseDoublePinyinToolTip}">
Icon="&#xf085;" <cc:ExCard.SideContent>
Sub="{DynamicResource ShouldUseDoublePinyinToolTip}"
Type="Middle">
<ui:ToggleSwitch <ui:ToggleSwitch
IsOn="{Binding UseDoublePinyin}" IsOn="{Binding UseDoublePinyin}"
OffContent="{DynamicResource disable}" OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" OnContent="{DynamicResource enable}"
ToolTip="{DynamicResource ShouldUseDoublePinyinToolTip}" /> ToolTip="{DynamicResource ShouldUseDoublePinyinToolTip}" />
</cc:Card> </cc:ExCard.SideContent>
<cc:Card <cc:Card Title="{DynamicResource DoublePinyinSchema}" Type="InsideFit">
Visibility="{ext:VisibleWhen {Binding UseDoublePinyin},
IsEqualToBool=True}"
Title="{DynamicResource DoublePinyinSchema}"
Sub="{DynamicResource DoublePinyinSchemaToolTip}"
Type="Last">
<ComboBox <ComboBox
DisplayMemberPath="Display" DisplayMemberPath="Display"
ItemsSource="{Binding DoublePinyinSchemas}" ItemsSource="{Binding DoublePinyinSchemas}"
SelectedValue="{Binding Settings.DoublePinyinSchema}" SelectedValue="{Binding Settings.DoublePinyinSchema}"
SelectedValuePath="Value" /> SelectedValuePath="Value" />
</cc:Card> </cc:Card>
</cc:CardGroup> </cc:ExCard>
<cc:Card <cc:Card
Title="{DynamicResource language}" Title="{DynamicResource language}"