Skip to content

Commit c04bebe

Browse files
committed
feat(Studio): Apply fuzzy matching to auto-complete entry search
1 parent da71913 commit c04bebe

5 files changed

Lines changed: 883 additions & 76 deletions

File tree

Studio/CelesteStudio/Editing/AutoCompletion/CommandAutoCompleteMenu.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ public override void Refresh(bool open = true) {
110110
Entries.AddRange(baseEntries);
111111
Filter = line;
112112

113+
editor.RecalcPopupMenu();
114+
113115
currArgumentIndex = -1;
114116
} else {
115117
var command = CommunicationWrapper.Commands.FirstOrDefault(cmd => string.Equals(cmd.Name, commandLine.Value.Command, StringComparison.OrdinalIgnoreCase));
@@ -306,7 +308,6 @@ await Application.Instance.InvokeAsync(() => {
306308
}, token);
307309
} else {
308310
Entries.Clear();
309-
editor.RecalcPopupMenu();
310311
}
311312

312313
if (editor.GetSelectedQuickEdit() is { } quickEdit && commandLine.Value.Arguments[^1] == quickEdit.DefaultText) {
@@ -315,6 +316,7 @@ await Application.Instance.InvokeAsync(() => {
315316
} else {
316317
Filter = commandLine.Value.Arguments[^1];
317318
}
319+
editor.RecalcPopupMenu();
318320
}
319321
}
320322
}

Studio/CelesteStudio/Editing/Editor.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1166,11 +1166,10 @@ protected override void OnTextInput(TextInputEventArgs e) {
11661166
if (oldCaret.Row != Document.Caret.Row) {
11671167
FixInvalidInput(oldCaret.Row);
11681168
}
1169-
1170-
DesiredVisualCol = Document.Caret.Col;
11711169
}
11721170

1173-
1171+
DesiredVisualCol = Document.Caret.Col;
1172+
autoCompleteMenu!.Refresh();
11741173
}
11751174

11761175
protected override void OnDelete(CaretMovementType direction) {

Studio/CelesteStudio/Editing/PopupMenu.cs

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ public int RecommendedWidth {
432432
return 0;
433433
}
434434

435-
int visibleEntryCount = Math.Min(shownEntries.Length, (int) MathF.Ceiling(ClientSize.Height / EntryHeight) + 1);
435+
int visibleEntryCount = Math.Min(shownEntries.Length, (int) MathF.Ceiling(ClientSize.Height / (float)EntryHeight) + 1);
436436
int medianGroupLen = shownEntries
437437
.Where((_, idx) => idx + visibleEntryCount <= shownEntries.Length)
438438
.Select((_, idx) => {
@@ -460,8 +460,8 @@ public int RecommendedWidth {
460460
private Entry[] shownEntries = [];
461461
private readonly ContentDrawable drawable;
462462

463-
private int TopVisibleEntry => (int) MathF.Floor(ScrollPosition.Y / EntryHeight);
464-
private int BottomVisibleEntry => (int) MathF.Ceiling((ScrollPosition.Y + ClientSize.Height) / EntryHeight);
463+
private int TopVisibleEntry => (int) MathF.Floor(ScrollPosition.Y / (float)EntryHeight);
464+
private int BottomVisibleEntry => (int) MathF.Ceiling((ScrollPosition.Y + ClientSize.Height) / (float)EntryHeight);
465465
private IEnumerable<Entry> VisibleEntries {
466466
get {
467467
int top = TopVisibleEntry;
@@ -471,6 +471,8 @@ private IEnumerable<Entry> VisibleEntries {
471471
}
472472
}
473473

474+
private readonly FuzzyMatcher matcher = new() { IgnoreCase = true };
475+
474476
public PopupMenu() {
475477
LoadStorage();
476478

@@ -493,35 +495,63 @@ public void Recalc() {
493495
// Order for the categories
494496
const int frequentlyUsedThreshold = 5;
495497

496-
const int favouriteIndex = 0;
497-
const int frequentlyUsedIndex = favouriteIndex + 1;
498-
const int suggestionIndex = frequentlyUsedIndex + FrequentlyUsedCategorySize;
499-
const int regularIndex = suggestionIndex + 1;
500-
501498
var frequentlyUsed = entries
502-
.Where(entry => (string.IsNullOrEmpty(entry.SearchText) || entry.SearchText.StartsWith(filter, StringComparison.InvariantCultureIgnoreCase))
503-
&& entry.Data is { } data && data.Usages.TryGetValue(entry.StorageName, out uint amount) && amount >= frequentlyUsedThreshold)
504-
.OrderBy(entry => entry.Data!.Usages[entry.StorageName]);
499+
.Where(entry => entry.Data is { } data && data.Usages.TryGetValue(entry.StorageName, out uint amount) && amount >= frequentlyUsedThreshold)
500+
.Select(entry => (Entry: entry, Score: matcher.GetMatch(entry.SearchText.AsSpan(), filter.AsSpan())))
501+
.Where(pair => pair.Score != null)
502+
.OrderBy(pair => pair.Entry.Data!.Usages[pair.Entry.StorageName] * 10 + pair.Score!.Value);
503+
505504
int i = 0;
506-
foreach (var entry in frequentlyUsed) {
505+
foreach (var (entry, _) in frequentlyUsed) {
507506
entry.FrequentlyUsedIndex = i++;
508507
}
509508

510509
shownEntries = entries
511-
.Where(entry => string.IsNullOrEmpty(entry.SearchText) || entry.SearchText.StartsWith(filter, StringComparison.InvariantCultureIgnoreCase))
512-
.OrderBy(entry => {
513-
if (entry.Data is { } data && data.Favourites.Contains(entry.StorageName)) {
514-
return favouriteIndex;
510+
.Select(entry => (Entry: entry, Score: matcher.GetMatch(entry.SearchText.AsSpan(), filter.AsSpan())))
511+
.Where(pair => pair.Score != null)
512+
.Order(Comparer<(Entry Entry, ushort? Score)>.Create((lhs, rhs) => {
513+
ushort lhsScore = lhs.Score!.Value;
514+
ushort rhsScore = rhs.Score!.Value;
515+
516+
bool lhsFavourite = lhs.Entry.Data is { } lhsData && lhsData.Favourites.Contains(lhs.Entry.StorageName);
517+
bool rhsFavourite = rhs.Entry.Data is { } rhsData && rhsData.Favourites.Contains(rhs.Entry.StorageName);
518+
if (lhsFavourite && !rhsFavourite) {
519+
return -1;
515520
}
516-
if (entry.FrequentlyUsedIndex is >= 0 and < FrequentlyUsedCategorySize) {
517-
return frequentlyUsedIndex + entry.FrequentlyUsedIndex;
521+
if (!lhsFavourite && rhsFavourite) {
522+
return 1;
518523
}
519-
if (entry.Suggestion) {
520-
return suggestionIndex;
524+
if (lhsFavourite && rhsFavourite) {
525+
return lhsScore - rhsScore;
521526
}
522527

523-
return regularIndex;
524-
})
528+
bool lhsFrequent = lhs.Entry.FrequentlyUsedIndex is >= 0 and < FrequentlyUsedCategorySize;
529+
bool rhsFrequent = rhs.Entry.FrequentlyUsedIndex is >= 0 and < FrequentlyUsedCategorySize;
530+
if (lhsFrequent && !rhsFrequent) {
531+
return -1;
532+
}
533+
if (!lhsFrequent && rhsFrequent) {
534+
return 1;
535+
}
536+
if (lhsFrequent && rhsFrequent) {
537+
return (lhs.Entry.FrequentlyUsedIndex * 10 + lhsScore) - (rhs.Entry.FrequentlyUsedIndex * 10 + rhsScore);
538+
}
539+
540+
bool lhsSuggestion = lhs.Entry.Suggestion;
541+
bool rhsSuggestion = rhs.Entry.Suggestion;
542+
if (lhsSuggestion && !rhsSuggestion) {
543+
return -1;
544+
}
545+
if (!lhsSuggestion && rhsSuggestion) {
546+
return 1;
547+
}
548+
if (lhsSuggestion && rhsSuggestion) {
549+
return lhsScore - rhsScore;
550+
}
551+
552+
return lhsScore - rhsScore;
553+
}))
554+
.Select(pair => pair.Entry)
525555
.ToArray();
526556

527557
if (shownEntries.Length == 0) {

0 commit comments

Comments
 (0)