Skip to content

Commit 17b2970

Browse files
committed
feat: add accent normalization with optimized memory usage and toggle option
Implemented a string normalization method to handle accented characters, improving search consistency and preventing query mismatches. Added an accent mapping dictionary for common diacritics Implemented normalization using Span<char> and stackalloc to reduce heap allocations and improve performance Introduced a user-controlled toggle (SensitiveAccents) to enable or disable normalization dynamically Prepared the system for cache-aware queries based on normalization settings
1 parent d2f8663 commit 17b2970

6 files changed

Lines changed: 74 additions & 25 deletions

File tree

Flow.Launcher.Infrastructure/StringMatcher.cs

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace Flow.Launcher.Infrastructure
1212
public class StringMatcher
1313
{
1414
private readonly MatchOption _defaultMatchOption = new();
15-
15+
private readonly Settings _settings;
1616
public SearchPrecisionScore UserSettingSearchPrecision { get; set; }
1717

1818
private readonly IAlphabet _alphabet;
@@ -21,6 +21,7 @@ public StringMatcher(IAlphabet alphabet, Settings settings)
2121
{
2222
_alphabet = alphabet;
2323
UserSettingSearchPrecision = settings.QuerySearchPrecision;
24+
_settings = settings;
2425
}
2526

2627
// This is a workaround to allow unit tests to set the instance
@@ -69,8 +70,6 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption
6970
return new MatchResult(false, UserSettingSearchPrecision);
7071

7172
query = query.Trim();
72-
query = RemoveAccents(query);
73-
stringToCompare = RemoveAccents(stringToCompare);
7473
TranslationMapping translationMapping = null;
7574
if (_alphabet is not null && _alphabet.ShouldTranslate(query))
7675
{
@@ -84,10 +83,16 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption
8483
int acronymsTotalCount = 0;
8584
int acronymsMatched = 0;
8685

86+
var fullStringToCompareAndNormalize = opt.IgnoreCase ? Normalize(stringToCompare) : stringToCompare;
87+
var queryWithoutCaseAndNormalize = opt.IgnoreCase ? Normalize(query) : query;
88+
8789
var fullStringToCompareWithoutCase = opt.IgnoreCase ? stringToCompare.ToLower() : stringToCompare;
8890
var queryWithoutCase = opt.IgnoreCase ? query.ToLower() : query;
8991

90-
var querySubstrings = queryWithoutCase.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
92+
var fullStringToCompare = _settings.SensitiveAccents ? fullStringToCompareAndNormalize : fullStringToCompareWithoutCase;
93+
var queryToCompare = _settings.SensitiveAccents ? queryWithoutCaseAndNormalize : queryWithoutCase;
94+
95+
var querySubstrings = queryToCompare.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
9196
int currentQuerySubstringIndex = 0;
9297
var currentQuerySubstring = querySubstrings[currentQuerySubstringIndex];
9398
var currentQuerySubstringCharacterIndex = 0;
@@ -103,7 +108,7 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption
103108
List<int> spaceIndices = new List<int>();
104109

105110
for (var compareStringIndex = 0;
106-
compareStringIndex < fullStringToCompareWithoutCase.Length;
111+
compareStringIndex < fullStringToCompare.Length;
107112
compareStringIndex++)
108113
{
109114
// If acronyms matching successfully finished, this gets the remaining not matched acronyms for score calculation
@@ -120,14 +125,14 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption
120125

121126
// To maintain a list of indices which correspond to spaces in the string to compare
122127
// To populate the list only for the first query substring
123-
if (fullStringToCompareWithoutCase[compareStringIndex] == ' ' && currentQuerySubstringIndex == 0)
128+
if (fullStringToCompare[compareStringIndex] == ' ' && currentQuerySubstringIndex == 0)
124129
spaceIndices.Add(compareStringIndex);
125130

126131
// Acronym Match
127132
if (IsAcronym(stringToCompare, compareStringIndex))
128133
{
129-
if (fullStringToCompareWithoutCase[compareStringIndex] ==
130-
queryWithoutCase[currentAcronymQueryIndex])
134+
if (fullStringToCompare[compareStringIndex] ==
135+
queryToCompare[currentAcronymQueryIndex])
131136
{
132137
acronymMatchData.Add(compareStringIndex);
133138
acronymsMatched++;
@@ -139,7 +144,7 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption
139144
if (IsAcronymCount(stringToCompare, compareStringIndex))
140145
acronymsTotalCount++;
141146

142-
if (allQuerySubstringsMatched || fullStringToCompareWithoutCase[compareStringIndex] !=
147+
if (allQuerySubstringsMatched || fullStringToCompare[compareStringIndex] !=
143148
currentQuerySubstring[currentQuerySubstringCharacterIndex])
144149
{
145150
matchFoundInPreviousLoop = false;
@@ -166,7 +171,7 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption
166171
var startIndexToVerify = compareStringIndex - currentQuerySubstringCharacterIndex;
167172

168173
if (AllPreviousCharsMatched(startIndexToVerify, currentQuerySubstringCharacterIndex,
169-
fullStringToCompareWithoutCase, currentQuerySubstring))
174+
fullStringToCompare, currentQuerySubstring))
170175
{
171176
matchFoundInPreviousLoop = true;
172177

@@ -237,23 +242,29 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption
237242
return new MatchResult(false, UserSettingSearchPrecision);
238243
}
239244

240-
private static string RemoveAccents(string value)
241-
{
242-
if (string.IsNullOrEmpty(value))
243-
return value;
244-
string normalized = value.Normalize(NormalizationForm.FormD);
245-
StringBuilder sb = new();
246245

247-
foreach (char c in normalized)
246+
private static readonly Dictionary<char, char> AccentMap = new()
247+
{
248+
['á'] = 'a', ['à'] = 'a', ['ã'] = 'a', ['â'] = 'a', ['ä'] = 'a', ['å'] = 'a',
249+
['é'] = 'e', ['è'] = 'e', ['ê'] = 'e', ['ë'] = 'e',
250+
['í'] = 'i', ['ì'] = 'i', ['î'] = 'i', ['ï'] = 'i',
251+
['ó'] = 'o', ['ò'] = 'o', ['õ'] = 'o', ['ô'] = 'o', ['ö'] = 'o',
252+
['ú'] = 'u', ['ù'] = 'u', ['û'] = 'u', ['ü'] = 'u',
253+
['ç'] = 'c',
254+
['ñ'] = 'n',
255+
['ý'] = 'y', ['ÿ'] = 'y'
256+
};
257+
public static string Normalize(string value)
258+
{
259+
Span<char> buffer = stackalloc char[value.Length];
260+
for (int i = 0; i < value.Length; i++)
248261
{
249-
var unicodedCategory = Char.GetUnicodeCategory(c);
250-
if (unicodedCategory != UnicodeCategory.NonSpacingMark)
251-
sb.Append(c);
262+
var c = char.ToLowerInvariant(value[i]);
263+
buffer[i] = AccentMap.TryGetValue(c, out var mapped) ? mapped : c;
252264
}
253265

254-
return sb.ToString().Normalize(NormalizationForm.FormC);
266+
return new string(buffer);
255267
}
256-
257268
private static bool IsAcronym(string stringToCompare, int compareStringIndex)
258269
{
259270
if (IsAcronymChar(stringToCompare, compareStringIndex) ||
@@ -301,12 +312,12 @@ private static int CalculateClosestSpaceIndex(List<int> spaceIndices, int firstM
301312
}
302313

303314
private static bool AllPreviousCharsMatched(int startIndexToVerify, int currentQuerySubstringCharacterIndex,
304-
string fullStringToCompareWithoutCase, string currentQuerySubstring)
315+
string fullStringToCompare, string currentQuerySubstring)
305316
{
306317
var allMatch = true;
307318
for (int indexToCheck = 0; indexToCheck < currentQuerySubstringCharacterIndex; indexToCheck++)
308319
{
309-
if (fullStringToCompareWithoutCase[startIndexToVerify + indexToCheck] !=
320+
if (fullStringToCompare[startIndexToVerify + indexToCheck] !=
310321
currentQuerySubstring[indexToCheck])
311322
{
312323
allMatch = false;

Flow.Launcher.Infrastructure/UserSettings/Settings.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,20 @@ public bool ShouldUsePinyin
359359
}
360360
}
361361

362+
private bool _sensitiveAccents = false;
363+
public bool SensitiveAccents
364+
{
365+
get => _sensitiveAccents;
366+
set
367+
{
368+
if (_sensitiveAccents != value)
369+
{
370+
_sensitiveAccents = value;
371+
OnPropertyChanged();
372+
}
373+
}
374+
}
375+
362376
private bool _useDoublePinyin = false;
363377
public bool UseDoublePinyin
364378
{

Flow.Launcher/Languages/en.xaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@
123123
<system:String x:Key="select">Select</system:String>
124124
<system:String x:Key="hideOnStartup">Hide Flow Launcher on startup</system:String>
125125
<system:String x:Key="hideOnStartupToolTip">Flow Launcher search window is hidden in the tray after starting up.</system:String>
126+
<system:String x:Key="sensitiveAccent">Enable accent sensitivity when searching for programs.</system:String>
127+
<system:String x:Key="sensitiveAccentToolTip">When this option is enabled, you will be able to find programs that contain accented characters more easily.</system:String>
126128
<system:String x:Key="hideNotifyIcon">Hide tray icon</system:String>
127129
<system:String x:Key="hideNotifyIconToolTip">When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window.</system:String>
128130
<system:String x:Key="querySearchPrecision">Query Search Precision</system:String>

Flow.Launcher/Languages/pt-br.xaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@
121121
<system:String x:Key="select">Selecionar</system:String>
122122
<system:String x:Key="hideOnStartup">Esconder Flow Launcher na inicialização</system:String>
123123
<system:String x:Key="hideOnStartupToolTip">Flow Launcher search window is hidden in the tray after starting up.</system:String>
124+
<system:String x:Key="sensitiveAccent">Ativar a distinção de acentuação na consulta de programas.</system:String>
125+
<system:String x:Key="sensitiveAccentToolTip">Ao ativar ou desativar esta opção você precisa reniciar o Flow para ser aplicado corretamente a configuração, você poderá encontrar programas que possuem acentuação com mais facilidade.</system:String>
124126
<system:String x:Key="hideNotifyIcon">Ocultar ícone da bandeja</system:String>
125127
<system:String x:Key="hideNotifyIconToolTip">Quando o ícone não está na bandeja, o menu de Configurações pode ser aberto ao clicar na janela de busca com o botão direito do mouse.</system:String>
126128
<system:String x:Key="querySearchPrecision">Precisão de Busca da Consulta</system:String>

Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,19 @@ public int SearchDelayTimeValue
196196
}
197197
}
198198

199+
public bool SensitiveAccents
200+
{
201+
get => Settings.SensitiveAccents;
202+
set
203+
{
204+
if (Settings.SensitiveAccents != value)
205+
{
206+
Settings.SensitiveAccents = value;
207+
OnPropertyChanged();
208+
}
209+
}
210+
}
211+
199212
public int MaxHistoryResultsToShowValue
200213
{
201214
get => Settings.MaxHistoryResultsToShowForHomePage;

Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,14 @@
9191
OffContent="{DynamicResource disable}"
9292
OnContent="{DynamicResource enable}" />
9393
</ui:SettingsCard>
94-
94+
<ui:SettingsCard
95+
Description="{DynamicResource sensitiveAccentToolTip}"
96+
Header="{DynamicResource sensitiveAccent}">
97+
<ui:ToggleSwitch
98+
IsOn="{Binding Settings.SensitiveAccents}"
99+
OffContent="{DynamicResource disable}"
100+
OnContent="{DynamicResource enable}" />
101+
</ui:SettingsCard>
95102
<ui:SettingsCard
96103
Margin="0 14 0 0"
97104
Description="{DynamicResource showAtTopmostToolTip}"

0 commit comments

Comments
 (0)