Merge pull request #2116 from alederer/sortFuzzyMatches

In command palette, sort matches by quality/exactness
This commit is contained in:
tildearrow 2024-09-25 13:52:00 -05:00 committed by GitHub
commit b2c1f8d919
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 239 additions and 86 deletions

View file

@ -25,20 +25,124 @@
#include <algorithm>
#include <ctype.h>
#include "../ta-log.h"
#include "util.h"
static inline bool matchFuzzy(const char* haystack, const char* needle) {
size_t h_i=0; // haystack idx
size_t n_i=0; // needle idx
while (needle[n_i]!='\0') {
for (; std::tolower(haystack[h_i])!=std::tolower(needle[n_i]); h_i++) {
if (haystack[h_i]=='\0')
return false;
}
n_i+=1;
struct MatchScore {
size_t charsBeforeNeedle=0;
size_t charsWithinNeedle=0;
static bool IsFirstPreferable(const MatchScore& a, const MatchScore& b) {
int aBetter;
aBetter=b.charsWithinNeedle-a.charsWithinNeedle;
if (aBetter!=0) return aBetter>0;
aBetter=b.charsBeforeNeedle-a.charsBeforeNeedle;
if (aBetter!=0) return aBetter>0;
return false;
}
return true;
};
struct MatchResult {
MatchScore score;
std::vector<int> highlightChars;
};
static bool charMatch(const char* a, const char* b) {
// stub for future utf8 support, possibly with matching for related chars?
return std::tolower(*a)==std::tolower(*b);
}
// #define MATCH_GREEDY
// #define RUN_MATCH_TEST
static bool matchFuzzy(const char* haystack, int haystackLen, const char* needle, int needleLen, MatchResult* result) {
if (needleLen==0) {
result->score.charsBeforeNeedle=0;
result->score.charsWithinNeedle=0;
result->highlightChars.clear();
return true;
}
std::vector<MatchResult> matchPool(needleLen+1);
std::vector<MatchResult*> unusedMatches(needleLen+1);
std::vector<MatchResult*> matchesByLen(needleLen+1);
for (int i=0; i<needleLen+1; i++) {
unusedMatches[i]=&matchPool[i];
matchesByLen[i]=NULL;
}
for (int hIdx=0; hIdx<haystackLen; hIdx++) {
// try to continue our in-flight valid matches
for (int matchLen=needleLen-1; matchLen>=0; matchLen--) {
MatchResult*& m=matchesByLen[matchLen];
// ignore null matches except for 0
if (matchLen>0 && !m) continue;
#ifdef MATCH_GREEDY
// in greedy mode, don't start any new matches once we've already started matching.
// this will still return the correct bool result, but its score could be much poorer
// than the optimal match. consider the case:
//
// find "gl" in "google"
//
// greedy will see the match "g...l.", which has charsWithinNeedle of 3, while the
// fully algorithm will find the tighter match "...gl.", which has
// charsWithinNeedle of 0
if (matchLen==0 && unusedMatches.size() < matchPool.size()) {
continue;
}
#endif
// check match!
if (charMatch(haystack+hIdx, needle+matchLen)) {
// pull a fresh match from the pool if necessary
if (matchLen==0) {
m=unusedMatches.back();
unusedMatches.pop_back();
m->score.charsBeforeNeedle=hIdx;
m->score.charsWithinNeedle=0;
m->highlightChars.clear();
}
m->highlightChars.push_back(hIdx);
// advance, replacing the previous match of an equal len, which can only have been
// worse because it existed before us, so we can prune it out
if (matchesByLen[matchLen+1]) {
unusedMatches.push_back(matchesByLen[matchLen+1]);
}
matchesByLen[matchLen+1]=m;
m=NULL;
} else {
// tally up charsWithinNeedle
if (matchLen>0) {
matchesByLen[matchLen]->score.charsWithinNeedle++;
}
}
}
}
if (matchesByLen[needleLen]) {
if (result) *result=*matchesByLen[needleLen];
return true;
}
return false;
}
#ifdef RUN_MATCH_TEST
static void matchFuzzyTest() {
String hay="a__i_a_i__o";
String needle="aio";
MatchResult match;
matchFuzzy(hay.c_str(), hay.length(), needle.c_str(), needle.length(), &match);
logI( "match.score.charsWithinNeedle: %d", match.score.charsWithinNeedle );
}
#endif
void FurnaceGUI::drawPalette() {
bool accepted=false;
@ -67,45 +171,52 @@ void FurnaceGUI::drawPalette() {
break;
}
#ifdef RUN_MATCH_TEST
matchFuzzyTest();
#endif
if (ImGui::InputTextWithHint("##CommandPaletteSearch",hint,&paletteQuery) || paletteFirstFrame) {
paletteSearchResults.clear();
std::vector<MatchScore> matchScores;
auto Evaluate=[&](int i, const char* name, int nameLen) {
MatchResult result;
if (matchFuzzy(name, nameLen, paletteQuery.c_str(), paletteQuery.length(), &result)) {
paletteSearchResults.emplace_back();
paletteSearchResults.back().id=i;
paletteSearchResults.back().highlightChars=std::move(result.highlightChars);
matchScores.push_back(result.score);
}
};
switch (curPaletteType) {
case CMDPAL_TYPE_MAIN:
for (int i=0; i<GUI_ACTION_MAX; i++) {
if (guiActions[i].defaultBind==-1) continue;
if (matchFuzzy(guiActions[i].friendlyName,paletteQuery.c_str())) {
paletteSearchResults.push_back(i);
}
if (guiActions[i].defaultBind==-1) continue; // not a bind
Evaluate(i,guiActions[i].friendlyName,strlen(guiActions[i].friendlyName));
}
break;
case CMDPAL_TYPE_RECENT:
for (int i=0; i<(int)recentFile.size(); i++) {
if (matchFuzzy(recentFile[i].c_str(),paletteQuery.c_str())) {
paletteSearchResults.push_back(i);
}
Evaluate(i,recentFile[i].c_str(),recentFile[i].length());
}
break;
case CMDPAL_TYPE_INSTRUMENTS:
case CMDPAL_TYPE_INSTRUMENT_CHANGE:
if (matchFuzzy(_("- None -"),paletteQuery.c_str())) {
paletteSearchResults.push_back(0);
}
case CMDPAL_TYPE_INSTRUMENT_CHANGE: {
const char* noneStr=_("- None -");
Evaluate(0,noneStr,strlen(noneStr));
for (int i=0; i<e->song.insLen; i++) {
String s=fmt::sprintf("%02X: %s", i, e->song.ins[i]->name.c_str());
if (matchFuzzy(s.c_str(),paletteQuery.c_str())) {
paletteSearchResults.push_back(i+1); // because over here ins=0 is 'None'
}
Evaluate(i+1,s.c_str(),s.length()); // because over here ins=0 is 'None'
}
break;
}
case CMDPAL_TYPE_SAMPLES:
for (int i=0; i<e->song.sampleLen; i++) {
if (matchFuzzy(e->song.sample[i]->name.c_str(),paletteQuery.c_str())) {
paletteSearchResults.push_back(i);
}
Evaluate(i,e->song.sample[i]->name.c_str(),e->song.sample[i]->name.length());
}
break;
@ -113,9 +224,7 @@ void FurnaceGUI::drawPalette() {
for (int i=0; availableSystems[i]; i++) {
int ds=availableSystems[i];
const char* sysname=getSystemName((DivSystem)ds);
if (matchFuzzy(sysname,paletteQuery.c_str())) {
paletteSearchResults.push_back(ds);
}
Evaluate(ds,sysname,strlen(sysname));
}
break;
@ -124,67 +233,110 @@ void FurnaceGUI::drawPalette() {
ImGui::CloseCurrentPopup();
break;
};
// sort indices by match quality
std::vector<int> sortingIndices(paletteSearchResults.size());
for (size_t i=0; i<sortingIndices.size(); ++i) sortingIndices[i]=(int)i;
std::sort(sortingIndices.begin(), sortingIndices.end(), [&](size_t a, size_t b) {
return MatchScore::IsFirstPreferable(matchScores[a], matchScores[b]);
});
// update paletteSearchResults from sorted indices (taking care not to stomp while we iterate
std::vector<PaletteSearchResult> paletteSearchResultsCopy=paletteSearchResults;
for (size_t i=0; i<sortingIndices.size(); ++i) paletteSearchResults[i]=paletteSearchResultsCopy[sortingIndices[i]];
}
ImVec2 avail=ImGui::GetContentRegionAvail();
avail.y-=ImGui::GetFrameHeightWithSpacing();
if (ImGui::BeginChild("CommandPaletteList",avail,false,0)) {
bool navigated=false;
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) && curPaletteChoice>0) {
curPaletteChoice-=1;
navigated=true;
}
if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
curPaletteChoice+=1;
navigated=true;
}
if (paletteSearchResults.size()>0 && curPaletteChoice<0) {
curPaletteChoice=0;
navigated=true;
}
if (curPaletteChoice>=(int)paletteSearchResults.size()) {
curPaletteChoice=paletteSearchResults.size()-1;
navigated=true;
}
for (int i=0; i<(int)paletteSearchResults.size(); i++) {
bool current=(i==curPaletteChoice);
int id=paletteSearchResults[i];
String s="???";
switch (curPaletteType) {
case CMDPAL_TYPE_MAIN:
s=guiActions[id].friendlyName;
break;
case CMDPAL_TYPE_RECENT:
s=recentFile[id].c_str();
break;
case CMDPAL_TYPE_INSTRUMENTS:
case CMDPAL_TYPE_INSTRUMENT_CHANGE:
if (id==0) {
s=_("- None -");
} else {
s=fmt::sprintf("%02X: %s", id-1, e->song.ins[id-1]->name.c_str());
}
break;
case CMDPAL_TYPE_SAMPLES:
s=e->song.sample[id]->name.c_str();
break;
case CMDPAL_TYPE_ADD_CHIP:
s=getSystemName((DivSystem)id);
break;
default:
logE(_("invalid command palette type"));
break;
};
if (ImGui::Selectable(s.c_str(),current)) {
curPaletteChoice=i;
accepted=true;
bool navigated=false;
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) && curPaletteChoice>0) {
curPaletteChoice-=1;
navigated=true;
}
if ((navigated || paletteFirstFrame) && current) ImGui::SetScrollHereY();
if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
curPaletteChoice+=1;
navigated=true;
}
if (paletteSearchResults.size()>0 && curPaletteChoice<0) {
curPaletteChoice=0;
navigated=true;
}
if (curPaletteChoice>=(int)paletteSearchResults.size()) {
curPaletteChoice=paletteSearchResults.size()-1;
navigated=true;
}
int columnCount=curPaletteType==CMDPAL_TYPE_MAIN ? 2 : 1;
if (ImGui::BeginTable("##commandPaletteTable",columnCount,ImGuiTableFlags_SizingStretchProp)) {
// ImGui::TableSetupColumn("##action",ImGuiTableColumnFlags_WidthStretch);
// ImGui::TableSetupColumn("##shortcut");
for (int i=0; i<(int)paletteSearchResults.size(); i++) {
bool current=(i==curPaletteChoice);
int id=paletteSearchResults[i].id;
String s="???";
switch (curPaletteType) {
case CMDPAL_TYPE_MAIN:
s=guiActions[id].friendlyName;
break;
case CMDPAL_TYPE_RECENT:
s=recentFile[id].c_str();
break;
case CMDPAL_TYPE_INSTRUMENTS:
case CMDPAL_TYPE_INSTRUMENT_CHANGE:
if (id==0) {
s=_("- None -");
} else {
s=fmt::sprintf("%02X: %s", id-1, e->song.ins[id-1]->name.c_str());
}
break;
case CMDPAL_TYPE_SAMPLES:
s=e->song.sample[id]->name.c_str();
break;
case CMDPAL_TYPE_ADD_CHIP:
s=getSystemName((DivSystem)id);
break;
default:
logE(_("invalid command palette type"));
break;
};
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::PushID(s.c_str());
bool selectable=ImGui::Selectable("##paletteSearchItem",current,ImGuiSelectableFlags_SpanAllColumns|ImGuiSelectableFlags_AllowOverlap);
const char* str=s.c_str();
size_t chCursor=0;
const std::vector<int>& highlights=paletteSearchResults[i].highlightChars;
for (size_t ch=0; ch<highlights.size(); ch++) {
ImGui::SameLine(0.0f,0.0f);
ImGui::Text("%.*s", (int)(highlights[ch]-chCursor), str+chCursor);
ImGui::SameLine(0.0f,0.0f);
ImGui::TextColored(uiColors[GUI_COLOR_ACCENT_PRIMARY], "%.1s", str+highlights[ch]);
chCursor=highlights[ch]+1;
}
ImGui::SameLine(0.0f,0.0f);
ImGui::Text("%.*s", (int)(s.length()-chCursor), str+chCursor);
if (curPaletteType==CMDPAL_TYPE_MAIN) {
ImGui::TableNextColumn();
ImGui::TextColored(uiColors[GUI_COLOR_TEXT_DISABLED], "%s", getKeyName(actionKeys[paletteSearchResults[i].id], true).c_str());
}
if (selectable) {
curPaletteChoice=i;
accepted=true;
}
ImGui::PopID();
if ((navigated || paletteFirstFrame) && current) ImGui::SetScrollHereY();
}
ImGui::EndTable();
}
}
ImGui::EndChild();
@ -206,7 +358,7 @@ void FurnaceGUI::drawPalette() {
if (accepted) {
if (paletteSearchResults.size()>0) {
int i=paletteSearchResults[curPaletteChoice];
int i=paletteSearchResults[curPaletteChoice].id;
switch (curPaletteType) {
case CMDPAL_TYPE_MAIN:
doAction(i);

View file

@ -1628,10 +1628,11 @@ class FurnaceGUI {
String mmlStringSNES[DIV_MAX_CHIPS];
String folderString;
struct PaletteSearchResult { int id; std::vector<int> highlightChars; };
std::vector<DivSystem> sysSearchResults;
std::vector<std::pair<DivSample*,bool>> sampleBankSearchResults;
std::vector<FurnaceGUISysDef> newSongSearchResults;
std::vector<int> paletteSearchResults;
std::vector<PaletteSearchResult> paletteSearchResults;
FixedQueue<String,32> recentFile;
std::vector<DivInstrumentType> makeInsTypeList;
std::vector<FurnaceGUIWaveSizeEntry> waveSizeList;