Browse Source

Add string::trim. Add string::fuzzyScore. Add fuzzy scoring to Module Browser.

tags/v1.0.0
Andrew Belt 5 years ago
parent
commit
207ca888ed
6 changed files with 199 additions and 23 deletions
  1. +1
    -1
      helper.py
  2. +6
    -0
      include/string.hpp
  3. +19
    -15
      src/app/ModuleBrowser.cpp
  4. +1
    -0
      src/main.cpp
  5. +5
    -5
      src/plugin/Plugin.cpp
  6. +167
    -2
      src/string.cpp

+ 1
- 1
helper.py View File

@@ -243,7 +243,7 @@ def create_module(slug):
try: try:
tree = xml.etree.ElementTree.parse(panel_filename) tree = xml.etree.ElementTree.parse(panel_filename)
except FileNotFoundError: except FileNotFoundError:
raise UserException(f"Panel not found at {panel_filename}")
raise UserException(f"Panel not found at {panel_filename}. Run this command with no arguments for instructions to create panels.")


components = panel_to_components(tree) components = panel_to_components(tree)
print(f"Components extracted from {panel_filename}") print(f"Components extracted from {panel_filename}")


+ 6
- 0
include/string.hpp View File

@@ -18,6 +18,7 @@ std::string f(const char *format, ...);
std::string lowercase(const std::string &s); std::string lowercase(const std::string &s);
/** Replaces all characters to uppercase letters */ /** Replaces all characters to uppercase letters */
std::string uppercase(const std::string &s); std::string uppercase(const std::string &s);
std::string trim(const std::string &s);
/** Truncates and adds "..." to a string, not exceeding `len` characters */ /** Truncates and adds "..." to a string, not exceeding `len` characters */
std::string ellipsize(const std::string &s, size_t len); std::string ellipsize(const std::string &s, size_t len);
bool startsWith(const std::string &str, const std::string &prefix); bool startsWith(const std::string &str, const std::string &prefix);
@@ -29,6 +30,11 @@ std::string filename(const std::string &path);
std::string basename(const std::string &path); std::string basename(const std::string &path);
/** Extracts the extension of a path */ /** Extracts the extension of a path */
std::string extension(const std::string &path); std::string extension(const std::string &path);
/** Scores how well a query matches a string.
A score of 0 means no match.
The score is arbitrary and is only meaningful for sorting.
*/
float fuzzyScore(const std::string &s, const std::string &query);


struct CaseInsensitiveCompare { struct CaseInsensitiveCompare {
bool operator()(const std::string &a, const std::string &b) const { bool operator()(const std::string &a, const std::string &b) const {


+ 19
- 15
src/app/ModuleBrowser.cpp View File

@@ -31,13 +31,7 @@ namespace app {
static std::set<plugin::Model*> sFavoriteModels; static std::set<plugin::Model*> sFavoriteModels;




bool isMatch(const std::string &s, const std::string &search) {
std::string s2 = string::lowercase(s);
std::string search2 = string::lowercase(search);
return (s2.find(search2) != std::string::npos);
}

static bool isModelMatch(plugin::Model *model, const std::string &search) {
static float modelScore(plugin::Model *model, const std::string &search) {
if (search.empty()) if (search.empty())
return true; return true;
std::string s; std::string s;
@@ -50,12 +44,13 @@ static bool isModelMatch(plugin::Model *model, const std::string &search) {
s += model->slug; s += model->slug;
s += " "; s += " ";
s += model->name; s += model->name;
for (const std::string &tag : model->tags) {
// TODO Normalize tag
s += tag;
s += " ";
}
return isMatch(s, search);
// for (const std::string &tag : model->tags) {
// s += " ";
// s += tag;
// }
float score = string::fuzzyScore(s, search);
DEBUG("%s %f", s.c_str(), score);
return score;
} }




@@ -374,12 +369,21 @@ struct ModuleBrowser : widget::OpaqueWidget {
} }


void setSearch(const std::string &search) { void setSearch(const std::string &search) {
std::string searchTrimmed = string::trim(search);
std::map<const Widget*, float> scores;
// Compute scores and set visibility
for (Widget *w : modelContainer->children) { for (Widget *w : modelContainer->children) {
ModelBox *modelBox = dynamic_cast<ModelBox*>(w); ModelBox *modelBox = dynamic_cast<ModelBox*>(w);
assert(modelBox); assert(modelBox);
bool match = isModelMatch(modelBox->model, search);
modelBox->visible = match;
float score = modelScore(modelBox->model, searchTrimmed);
scores[modelBox] = score;
modelBox->visible = (score > 0);
} }
DEBUG("");
// Sort by score
modelContainer->children.sort([&](const Widget *w1, const Widget *w2) {
return scores[w1] > scores[w2];
});
// Reset scroll position // Reset scroll position
modelScroll->offset = math::Vec(); modelScroll->offset = math::Vec();
} }


+ 1
- 0
src/main.cpp View File

@@ -14,6 +14,7 @@
#include "patch.hpp" #include "patch.hpp"
#include "ui.hpp" #include "ui.hpp"
#include "system.hpp" #include "system.hpp"
#include "string.hpp"


#include <osdialog.h> #include <osdialog.h>
#include <thread> #include <thread>


+ 5
- 5
src/plugin/Plugin.cpp View File

@@ -77,14 +77,14 @@ void Plugin::fromJson(json_t *rootJ) {
size_t moduleId; size_t moduleId;
json_t *moduleJ; json_t *moduleJ;
json_array_foreach(modulesJ, moduleId, moduleJ) { json_array_foreach(modulesJ, moduleId, moduleJ) {
json_t *slugJ = json_object_get(rootJ, "slug");
if (!slugJ)
json_t *modelSlugJ = json_object_get(moduleJ, "slug");
if (!modelSlugJ)
continue; continue;
std::string slug = json_string_value(slugJ);
std::string modelSlug = json_string_value(modelSlugJ);


Model *model = getModel(slug);
Model *model = getModel(modelSlug);
if (!model) { if (!model) {
WARN("plugin.json contains module \"%s\" but it is not defined in the plugin", slug);
WARN("plugin.json of \"%s\" contains module \"%s\" but it is not defined in the plugin", slug.c_str(), modelSlug.c_str());
continue; continue;
} }




+ 167
- 2
src/string.cpp View File

@@ -1,4 +1,5 @@
#include "string.hpp" #include "string.hpp"
#include <cctype> // for tolower and toupper
#include <algorithm> // for transform #include <algorithm> // for transform
#include <libgen.h> // for dirname and basename #include <libgen.h> // for dirname and basename


@@ -26,16 +27,27 @@ std::string f(const char *format, ...) {


std::string lowercase(const std::string &s) { std::string lowercase(const std::string &s) {
std::string r = s; std::string r = s;
std::transform(r.begin(), r.end(), r.begin(), [](unsigned char c){ return std::tolower(c); });
std::transform(r.begin(), r.end(), r.begin(), [](unsigned char c) { return std::tolower(c); });
return r; return r;
} }


std::string uppercase(const std::string &s) { std::string uppercase(const std::string &s) {
std::string r = s; std::string r = s;
std::transform(r.begin(), r.end(), r.begin(), [](unsigned char c){ return std::toupper(c); });
std::transform(r.begin(), r.end(), r.begin(), [](unsigned char c) { return std::toupper(c); });
return r; return r;
} }


std::string trim(const std::string &s) {
const std::string whitespace = " \n\r\t";
size_t first = s.find_first_not_of(whitespace);
if (first == std::string::npos)
return "";
size_t last = s.find_last_not_of(whitespace);
if (last == std::string::npos)
return "";
return s.substr(first, last - first + 1);
}

std::string ellipsize(const std::string &s, size_t len) { std::string ellipsize(const std::string &s, size_t len) {
if (s.size() <= len) if (s.size() <= len)
return s; return s;
@@ -82,5 +94,158 @@ std::string extension(const std::string &path) {
} }




/*
From https://github.com/forrestthewoods/lib_fts by Forrest Smith
License:

This software is dual-licensed to the public domain and under the following
license: you are granted a perpetual, irrevocable license to copy, modify,
publish, and distribute this file as you see fit.
*/
static bool fuzzy_match_recursive(const char *pattern, const char *str, int &outScore, const char *strBegin, uint8_t const *srcMatches, uint8_t *matches, int maxMatches, int nextMatch, int &recursionCount, int recursionLimit) {
// Count recursions
++recursionCount;
if (recursionCount >= recursionLimit)
return false;

// Detect end of strings
if (*pattern == '\0' || *str == '\0')
return false;

// Recursion params
bool recursiveMatch = false;
uint8_t bestRecursiveMatches[256];
int bestRecursiveScore = 0;

// Loop through pattern and str looking for a match
bool first_match = true;
while (*pattern != '\0' && *str != '\0') {

// Found match
if (std::tolower(*pattern) == std::tolower(*str)) {

// Supplied matches buffer was too short
if (nextMatch >= maxMatches)
return false;

// "Copy-on-Write" srcMatches into matches
if (first_match && srcMatches) {
std::memcpy(matches, srcMatches, nextMatch);
first_match = false;
}

// Recursive call that "skips" this match
uint8_t recursiveMatches[256];
int recursiveScore;
if (fuzzy_match_recursive(pattern, str + 1, recursiveScore, strBegin, matches, recursiveMatches, sizeof(recursiveMatches), nextMatch, recursionCount, recursionLimit)) {

// Pick best recursive score
if (!recursiveMatch || recursiveScore > bestRecursiveScore) {
std::memcpy(bestRecursiveMatches, recursiveMatches, 256);
bestRecursiveScore = recursiveScore;
}
recursiveMatch = true;
}

// Advance
matches[nextMatch++] = (uint8_t)(str - strBegin);
++pattern;
}
++str;
}

// Determine if full pattern was matched
bool matched = *pattern == '\0' ? true : false;

// Calculate score
if (matched) {
const int sequential_bonus = 5; // bonus for adjacent matches
const int separator_bonus = 30; // bonus if match occurs after a separator
const int camel_bonus = 0; // bonus if match is uppercase and prev is lower
const int first_letter_bonus = 15; // bonus if the first letter is matched

const int leading_letter_penalty = 0; // penalty applied for every letter in str before the first match
const int max_leading_letter_penalty = 0; // maximum penalty for leading letters
const int unmatched_letter_penalty = -1; // penalty for every letter that doesn't matter

// Iterate str to end
while (*str != '\0')
++str;

// Initialize score
outScore = 100;

// Apply leading letter penalty
int penalty = leading_letter_penalty * matches[0];
if (penalty < max_leading_letter_penalty)
penalty = max_leading_letter_penalty;
outScore += penalty;

// Apply unmatched penalty
int unmatched = (int)(str - strBegin) - nextMatch;
outScore += unmatched_letter_penalty * unmatched;

// Apply ordering bonuses
for (int i = 0; i < nextMatch; ++i) {
uint8_t currIdx = matches[i];

if (i > 0) {
uint8_t prevIdx = matches[i - 1];

// Sequential
if (currIdx == (prevIdx + 1))
outScore += sequential_bonus;
}

// Check for bonuses based on neighbor character value
if (currIdx > 0) {
// Camel case
char neighbor = strBegin[currIdx - 1];
char curr = strBegin[currIdx];
if (std::islower(neighbor) && std::isupper(curr))
outScore += camel_bonus;

// Separator
bool neighborSeparator = neighbor == '_' || neighbor == ' ';
if (neighborSeparator)
outScore += separator_bonus;
}
else {
// First letter
outScore += first_letter_bonus;
}
}
}

// Return best result
if (recursiveMatch && (!matched || bestRecursiveScore > outScore)) {
// Recursive score is better than "this"
std::memcpy(matches, bestRecursiveMatches, maxMatches);
outScore = bestRecursiveScore;
return true;
}
else if (matched) {
// "this" score is better than recursive
return true;
}
else {
// no match
return false;
}
}


float fuzzyScore(const std::string &s, const std::string &query) {
uint8_t matches[256];
int recursionCount = 0;
int recursionLimit = 10;
int score = 0;
bool match = fuzzy_match_recursive(query.c_str(), s.c_str(), score, s.c_str(),
NULL, matches, sizeof(matches),
0, recursionCount, recursionLimit);
return match ? score : 0.f;
}


} // namespace string } // namespace string
} // namespace rack } // namespace rack

Loading…
Cancel
Save