Browse Source

Replaced fuzzy algorithm, added cached list functionality

* Replaced the old fuzzy algorithm with a similar non-recursive
version. This is to make it easier for me to make tweaks. Results
still aren't perfect
* Added some commented code related to highlighting match letters
which doesn't work yet
* Added cross-compile for windows to Jamrules
master
Macoy Madson 5 years ago
parent
commit
fc268fc08a
  1. 20
      Jamrules
  2. 15
      README.org
  3. 279
      fuzzy.c
  4. 40
      fuzzy.h
  5. 300
      macoyFuzzy.c
  6. 105
      macoyFuzzy.el
  7. 26
      macoyFuzzyTests.el

20
Jamrules

@ -30,8 +30,24 @@ AR = ar cr ;
OPTIM = -O0 ;
HDRS = . ../emacs/emacs/src ;
if $(EMACS_SRC_DIR)
{
HDRS = . $(EMACS_SRC_DIR) ;
}
else
{
HDRS = . ../../3rdParty/repositories/emacs/src ;
}
# E.g.
# jam -j4 -q -sCROSS_COMPILE_WINDOWS=true
if $(CROSS_COMPILE_WINDOWS)
{
CC = x86_64-w64-mingw32-gcc ;
LINK = x86_64-w64-mingw32-gcc ;
AR = x86_64-w64-mingw32-ar ;
SUFSHR = .dll ;
}
# Some helpful Jam commands
# -q : stop on failed target

15
README.org

@ -7,6 +7,7 @@ I created this because I was encountering performance problems with ELisp-based
1. To build, make sure you have Jam installed:
: sudo apt install jam
2. Open Jamrules and add the path to your ~emacs/src~ to the ~HDRS~ variable. Ensure there is a space between the ~;~ and your path
You can also run ~jam -sEMACS_SRC_DIR=path/to/your/emacs/src~ instead (ignore step 3).
3. Run ~jam~ in emacs-fuzzy-module directory
4. Open macoyFuzzy.el and make the ~module-load~ path reference ~macoyFuzzy.so~ (wherever you built it)
5. Evaluate macoyFuzzy.el
@ -18,8 +19,14 @@ I created this because I was encountering performance problems with ELisp-based
(macoy-fuzzy-ido-mode 1)
#+END_SRC
* Building on Windows
The easiest way to build is to use Linux and cross compile via mingw64. To do this, just set ~CROSS_COMPILE_WINDOWS~ when building to automatically use the mingw toolset:
~jam -sCROSS_COMPILE_WINDOWS=true~
If you don't have jam, you can build things manually (this list may get out of sync):
- ~gcc -c fuzzy.c -ggdb3 -Wall -fPIC -O0 -I. -I. -I../emacs/emacs/src~
- ~gcc -c utils.c -ggdb3 -Wall -fPIC -O0 -I. -I. -I../emacs/emacs/src~
- ~gcc -c macoyFuzzy.c -ggdb3 -Wall -fPIC -O0 -I. -I. -I../emacs/emacs/src~
- ~gcc -shared -o macoyFuzzy.dll fuzzy.o utils.o macoyFuzzy.o~
If you do have Jam but it complains about the environment, set the environment first, e.g. this is what I did:
~env JAM_TOOLSET=MINGW ../../nonRepos/jam-2.6/jam.exe -j4 -q~
gcc -c fuzzy.c -ggdb3 -Wall -fPIC -O0 -I. -I. -I../emacs/emacs/src
gcc -c utils.c -ggdb3 -Wall -fPIC -O0 -I. -I. -I../emacs/emacs/src
gcc -c macoyFuzzy.c -ggdb3 -Wall -fPIC -O0 -I. -I. -I../emacs/emacs/src
gcc -shared -o macoyFuzzy.dll fuzzy.o utils.o macoyFuzzy.o

279
fuzzy.c

@ -14,218 +14,61 @@
#include <stdio.h>
#endif
static bool fuzzy_match_recursive(const char* pattern, const char* str, int* outScore,
const char* strBegin, unsigned char const* srcMatches,
unsigned char* newMatches, int maxMatches, int nextMatch,
int* recursionCount, int recursionLimit);
// Public interface
bool fuzzy_match_simple(char const* pattern, char const* str)
{
while (*pattern != '\0' && *str != '\0')
{
if (tolower(*pattern) == tolower(*str))
++pattern;
++str;
}
return *pattern == '\0' ? true : false;
}
bool fuzzy_match(char const* pattern, char const* str, int* outScore)
{
unsigned char matches[256];
return fuzzy_match_with_matches(pattern, str, outScore, matches, sizeof(matches));
}
bool fuzzy_match_with_matches(char const* pattern, char const* str, int* outScore,
unsigned char* matches, int maxMatches)
static bool fuzzy_StringRegionEquals(char const* aBegin, char const* aEnd, char const* bBegin,
char const* bEnd)
{
int recursionCount = 0;
int recursionLimit = 10;
return fuzzy_match_recursive(pattern, str, outScore, str, NULL, matches, maxMatches, 0,
&recursionCount, recursionLimit);
}
// Private implementation
static bool fuzzy_match_recursive(const char* pattern, const char* str, int* outScore,
const char* strBegin, unsigned char const* srcMatches,
unsigned char* matches, int maxMatches, int nextMatch,
int* recursionCount, int recursionLimit)
{
// Count recursions
++(*recursionCount);
if (*recursionCount >= recursionLimit)
if (!aBegin || !aEnd || !bBegin || !bEnd)
return false;
// Detect end of strings
if (*pattern == '\0' || *str == '\0')
if (aEnd - aBegin != bEnd - bBegin)
return false;
// Recursion params
bool recursiveMatch = false;
unsigned char bestRecursiveMatches[256];
int bestRecursiveScore = 0;
// Loop through pattern and str looking for a match
bool first_match = true;
while (*pattern != '\0' && *str != '\0')
char const* bIter = bBegin;
for (char const* aIter = aBegin; aIter != aEnd && bIter != bEnd; ++aIter, ++bIter)
{
// Found match
if (tolower(*pattern) == tolower(*str))
{
// Supplied matches buffer was too short
if (nextMatch >= maxMatches)
return false;
// "Copy-on-Write" srcMatches into matches
if (first_match && srcMatches)
{
memcpy(matches, srcMatches, nextMatch);
first_match = false;
}
// Recursive call that "skips" this match
unsigned char 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)
{
memcpy(bestRecursiveMatches, recursiveMatches, 256);
bestRecursiveScore = recursiveScore;
}
recursiveMatch = true;
}
// Advance
matches[nextMatch++] = (unsigned char)(str - strBegin);
++pattern;
}
++str;
}
// Determine if full pattern was matched
bool matched = *pattern == '\0' ? true : false;
// Calculate score
if (matched)
{
const int sequential_bonus = 15; // bonus for adjacent matches
const int separator_bonus = 30; // bonus if match occurs after a separator
const int camel_bonus = 30; // bonus if match is uppercase and prev is lower
const int first_letter_bonus = 15; // bonus if the first letter is matched
// penalty applied for every letter in str before the first match
const int leading_letter_penalty = -5;
const int max_leading_letter_penalty = -15; // 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)
{
unsigned char currIdx = matches[i];
if (i > 0)
{
unsigned char 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 (islower(neighbor) && isupper(curr))
*outScore += camel_bonus;
// Separator
bool neighborSeparator = neighbor == '_' || neighbor == ' ';
if (neighborSeparator)
*outScore += separator_bonus;
}
else
{
// First letter
*outScore += first_letter_bonus;
}
}
if (*aIter != *bIter)
return false;
}
// Return best result
if (recursiveMatch && (!matched || bestRecursiveScore > *outScore))
{
// Recursive score is better than "this"
memcpy(matches, bestRecursiveMatches, maxMatches);
*outScore = bestRecursiveScore;
return true;
}
else if (matched)
{
// "this" score is better than recursive
return true;
}
else
{
// no match
return false;
}
return true;
}
#define MAX1(max, compare) max = (compare) > (max) ? (compare) : (max)
bool strRegionEquals(char const* aBegin, char const* aEnd, char const* bBegin, char const* bEnd)
static int fuzzy_Max(int a, int b)
{
if (!aBegin || !aEnd || !bBegin || !bEnd)
return false;
if (aEnd - aBegin != bEnd - bBegin)
return false;
return a > b ? a : b;
}
for (char const* aIter = aBegin; aIter != aEnd; ++aIter)
static void fuzzy_ScoreString_ConsecutiveMatch(
char const* strIter, char const* consecutiveMatchStrIter, int* bestConsecutiveMatch,
char const** bestMatchStrIter, char const** bestMatchStrIterEnd, int* totalConsecutiveMatches)
{
// Don't count single characters as consecutive matches
if (consecutiveMatchStrIter - strIter > 1)
{
for (char const* bIter = bBegin; bIter != bEnd; ++bIter)
// Only count as a different consecutive match if it is equal to or larger than the
// bestConsecutiveMatch and the string doesn't equal the previous largest consecutive match.
// This is to prevent multiple identical matches scoring highly
if (consecutiveMatchStrIter - strIter >= *bestConsecutiveMatch &&
!fuzzy_StringRegionEquals(strIter, consecutiveMatchStrIter, *bestMatchStrIter,
*bestMatchStrIterEnd))
{
if (*aIter != *bIter)
return false;
*bestMatchStrIter = strIter;
*bestMatchStrIterEnd = consecutiveMatchStrIter;
*totalConsecutiveMatches += consecutiveMatchStrIter - strIter;
}
}
return true;
// Add to the score how far we got with matches
*bestConsecutiveMatch = fuzzy_Max(*bestConsecutiveMatch, consecutiveMatchStrIter - strIter);
}
}
bool fuzzy_match_better(char const* pattern, char const* str, int* outScore)
bool fuzzy_ScoreString(char const* pattern, char const* str, int* outScore)
{
const int initialScore = 50;
const int firstCharacterValue = 10;
const int acronymCharacterValue = 8;
const int acronymCharacterValue = 10;
const int bestConsecutiveMatchMultiplier = 2;
const float perfectPrefixMatchMultiplier = 1.2f;
// Note that totalConsecutiveMatches will not be a a linear value, i.e. if you have query 'tes'
// and you're scoring 'tester' totalConsecutiveMatches will be 7 ('tes', 'es', 'te')
const int totalConsecutiveMatchesMultiplier = 1;
@ -292,59 +135,35 @@ bool fuzzy_match_better(char const* pattern, char const* str, int* outScore)
++consecutiveMatchStrIter;
else
{
// Don't count single characters as consecutive matches
if (consecutiveMatchStrIter - strIter > 1)
{
// Only count as a different consecutive match if it is equal to or larger
// than the bestConsecutiveMatch and the string doesn't equal the previous
// largest consecutive match. This is to prevent multiple identical matches
// scoring highly. My test case was query "orgpro" where "org-do-promote"
// would score lower than "org-org-export-as-org" because of all the "org"s
if (consecutiveMatchStrIter - strIter >= bestConsecutiveMatch &&
!strRegionEquals(strIter, consecutiveMatchStrIter, bestMatchStrIter,
bestMatchStrIterEnd))
{
bestMatchStrIter = strIter;
bestMatchStrIterEnd = consecutiveMatchStrIter;
totalConsecutiveMatches += consecutiveMatchStrIter - strIter;
}
// Add to the score how far we got with matches
MAX1(bestConsecutiveMatch, consecutiveMatchStrIter - strIter);
}
fuzzy_ScoreString_ConsecutiveMatch(
strIter, consecutiveMatchStrIter, &bestConsecutiveMatch, &bestMatchStrIter,
&bestMatchStrIterEnd, &totalConsecutiveMatches);
consecutiveMatchStrIter = strIter;
}
}
// Don't count single characters as consecutive matches
if (consecutiveMatchStrIter - strIter > 1)
{
// Only count as a different consecutive match if it is equal to or larger than the
// bestConsecutiveMatch and the string doesn't equal the previous largest
// consecutive match
if (consecutiveMatchStrIter - strIter >= bestConsecutiveMatch &&
!strRegionEquals(strIter, consecutiveMatchStrIter, bestMatchStrIter,
bestMatchStrIterEnd))
{
bestMatchStrIter = strIter;
bestMatchStrIterEnd = consecutiveMatchStrIter;
totalConsecutiveMatches += consecutiveMatchStrIter - strIter;
}
// Add to the score how far we got with matches
MAX1(bestConsecutiveMatch, consecutiveMatchStrIter - strIter);
}
fuzzy_ScoreString_ConsecutiveMatch(strIter, consecutiveMatchStrIter,
&bestConsecutiveMatch, &bestMatchStrIter,
&bestMatchStrIterEnd, &totalConsecutiveMatches);
}
// If the best match is the first characters, give an extra bonus
int perfectPrefixMatch = 0;
int bestMatchLength = bestMatchStrIterEnd - bestMatchStrIter;
if (fuzzy_StringRegionEquals(str, str + bestMatchLength, bestMatchStrIter,
bestMatchStrIterEnd))
perfectPrefixMatch = bestMatchLength * perfectPrefixMatchMultiplier;
consecutiveMatchesScore = (bestConsecutiveMatch * bestConsecutiveMatchMultiplier) +
(totalConsecutiveMatches * totalConsecutiveMatchesMultiplier);
(totalConsecutiveMatches * totalConsecutiveMatchesMultiplier) +
perfectPrefixMatch;
}
// Total score. Penalize longer strings
*outScore = (initialScore - strLength) + (acronymScore + consecutiveMatchesScore);
#ifdef FUZZY_MATCH_DEBUG
#if FUZZY_MATCH_DEBUG
printf("%s query %s initial %d length %d acronymScore %d consecutiveMatchesScore %d FINAL %d\n",
str, pattern, initialScore, strLength, acronymScore, consecutiveMatchesScore, *outScore);
#endif

40
fuzzy.h

@ -1,43 +1,5 @@
#pragma once
// 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.
//
// VERSION
// 0.2.0 (2017-02-18) Scored matches perform exhaustive search for best score
// 0.1.0 (2016-03-28) Initial release
//
// AUTHOR
// Forrest Smith
//
// NOTES
// Compiling
// You MUST add '#define FTS_FUZZY_MATCH_IMPLEMENTATION' before including this header in ONE
// source file to create implementation.
//
// fuzzy_match_simple(...)
// Returns true if each character in pattern is found sequentially within str
//
// fuzzy_match(...)
// Returns true if pattern is found AND calculates a score.
// Performs exhaustive search via recursion to find all possible matches and match with highest
// score.
// Scores values have no intrinsic meaning. Possible score range is not normalized and varies
// with pattern.
// Recursion is limited internally (default=10) to prevent degenerate cases (pattern="aaaaaa"
// str="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
// Uses uint8_t for match indices. Therefore patterns are limited to 256 characters.
// Score system should be tuned for YOUR use case. Words, sentences, file names, or method names
// all prefer different tuning.
#include <stdbool.h>
bool fuzzy_match_simple(char const* pattern, char const* str);
bool fuzzy_match(char const* pattern, char const* str, int* outScore);
bool fuzzy_match_with_matches(char const* pattern, char const* str, int* outScore,
unsigned char* matches, int maxMatches);
// Macoy's version
bool fuzzy_match_better(char const* pattern, char const* str, int* outScore);
bool fuzzy_ScoreString(char const* pattern, char const* str, int* outScore);

300
macoyFuzzy.c

@ -5,13 +5,14 @@
#include <emacs-module.h>
#include <stdlib.h>
#include <string.h>
#include "fuzzy.h"
#include "utils.h"
// Uncomment for printf debug output
//#define MACOY_FUZZY_DEBUG
/* #define MACOY_FUZZY_DEBUG */
#include <stdio.h>
#ifdef MACOY_FUZZY_DEBUG
#include <stdio.h>
#endif
@ -19,7 +20,7 @@
int plugin_is_GPL_compatible;
// For easy switching of which algorithm to use
#define FUZZY_MATCH(query, string, outScore) fuzzy_match_better(query, string, outScore)
#define FUZZY_MATCH(query, string, outScore) fuzzy_ScoreString(query, string, outScore)
// Note that for now this will affect the quality of the results if e.g. the best result is actually
// match #2049. I'll have to make this good eventually
@ -58,21 +59,67 @@ emacs_value makeListFromFuzzyMatches(emacs_env* env, MacoyFuzzyMatch** matches,
return listObject;
}
static emacs_value fuzzy_MakeEmptyList(emacs_env* env)
{
emacs_value emptyList[] = {};
return env->funcall(env, env->intern(env, "list"), 0, emptyList);
}
static emacs_value fuzzy_ProcessMatches(emacs_env* env, char* queryBuffer,
MacoyFuzzyMatch matches[], int numMatches)
{
if (numMatches)
{
MacoyFuzzyMatch** sortedMatches = sortFuzzyMatches(matches, numMatches);
#ifdef MACOY_FUZZY_DEBUG
printf("\nQuery: %s\n", queryBuffer);
for (int i = 0; i < numMatches; ++i)
{
char* stringBuffer = NULL;
size_t stringBufferSize = 0;
copy_string_contents(env, sortedMatches[i]->string, &stringBuffer, &stringBufferSize);
printf("%s score: %d\n", stringBuffer, sortedMatches[i]->score);
free(stringBuffer);
}
#endif
emacs_value matchesList = makeListFromFuzzyMatches(env, sortedMatches, numMatches);
free(sortedMatches);
free(queryBuffer);
return matchesList;
}
else
{
free(queryBuffer);
return fuzzy_MakeEmptyList(env);
}
}
// Takes a query string and a vector of strings and returns a list of strings matching the query
// TODO: Make the values cached instead of having to copy the strings over every time!
static emacs_value FmacoyFuzzyFilterVector_fun(emacs_env* env, ptrdiff_t nargs, emacs_value args[],
void* data)
{
// Get the string arguments
// Get the query argument
char* queryBuffer = NULL;
size_t queryBufferSize = 0;
copy_string_contents(env, args[0], &queryBuffer, &queryBufferSize);
// If the third argument is set to anything, give bonus points to recently selected items
#ifdef MACOY_FUZZY_DEBUG
printf("passed %d args", (int)nargs);
#endif
bool isInReverseOrderOfRecentlySelected = (nargs == 3);
// TODO: Make this resizeable or come up with a way to toss out bad scores?
MacoyFuzzyMatch matches[MAX_MATCHES];
int numMatches = 0;
for (int i = 0; i < env->vec_size(env, args[1]); ++i)
int numStringsToScore = env->vec_size(env, args[1]);
// Iterate in reverse because if isInReverseOrderOfRecentlySelected is true, we want to make
// sure we get all the best suggestions in. If that isn't true, it's still fine to go in reverse
for (int i = numStringsToScore - 1; i >= 0; --i)
{
emacs_value currentString = env->vec_get(env, args[1], i);
@ -95,40 +142,32 @@ static emacs_value FmacoyFuzzyFilterVector_fun(emacs_env* env, ptrdiff_t nargs,
MacoyFuzzyMatch* currentMatch = &matches[numMatches++];
currentMatch->string = currentString;
currentMatch->score = score;
// Assign history/recently selected bonus
if (isInReverseOrderOfRecentlySelected)
{
// Extra bonus for very recent stuff
if (numStringsToScore - i <= 20)
currentMatch->score += 20 - (numStringsToScore - i);
int inTop100Bonus = 10;
if (numStringsToScore - i <= 100)
currentMatch->score += inTop100Bonus;
#ifdef MACOY_FUZZY_DEBUG
if (currentMatch->score != score)
printf("query %s str %s score %d + history bonus %d final %d", queryBuffer,
stringToCheckBuffer, score, currentMatch->score - score,
currentMatch->score);
#endif
}
}
// Reached max number of matches
else
break;
}
if (numMatches)
{
MacoyFuzzyMatch** sortedMatches = sortFuzzyMatches(matches, numMatches);
#ifdef MACOY_FUZZY_DEBUG
printf("\nQuery: %s\n", queryBuffer);
for (int i = 0; i < numMatches; ++i)
{
char* stringBuffer = NULL;
size_t stringBufferSize = 0;
copy_string_contents(env, sortedMatches[i]->string, &stringBuffer, &stringBufferSize);
printf("%s score: %d\n", stringBuffer, sortedMatches[i]->score);
free(stringBuffer);
}
#endif
emacs_value matchesList = makeListFromFuzzyMatches(env, sortedMatches, numMatches);
free(sortedMatches);
free(queryBuffer);
return matchesList;
}
else
{
emacs_value emptyList[] = {};
free(queryBuffer);
return env->funcall(env, env->intern(env, "list"), 0, emptyList);
}
return fuzzy_ProcessMatches(env, queryBuffer, matches, numMatches);
}
static emacs_value FmacoyFuzzyScore_fun(emacs_env* env, ptrdiff_t nargs, emacs_value args[],
@ -151,16 +190,205 @@ static emacs_value FmacoyFuzzyScore_fun(emacs_env* env, ptrdiff_t nargs, emacs_v
return env->make_integer(env, outScore);
}
// TODO Cache history
typedef struct FuzzyCachedList
{
char* name;
char* packedStrings;
size_t* stringLengths;
int numStrings;
} FuzzyCachedList;
#define MAX_CACHED_LISTS 10
static FuzzyCachedList g_fuzzyCachedLists[MAX_CACHED_LISTS] = {0};
static emacs_value FmacoyFuzzyCacheVector_fun(emacs_env* env, ptrdiff_t nargs, emacs_value args[],
void* data)
{
// Get the name argument
char* nameBuffer = NULL;
size_t nameBufferSize = 0;
copy_string_contents(env, args[0], &nameBuffer, &nameBufferSize);
int numStrings = env->vec_size(env, args[1]);
char** unpackedStrings = malloc(sizeof(char*) * numStrings);
size_t* stringLengths = malloc(sizeof(size_t) * numStrings);
size_t packedStringsTotal = 0;
// We're going to pack the strings contiguously. Will this help speed things up during
// filtering? Maybe. Either way, we don't care too much if the caching code is a bit slow
for (int i = 0; i < numStrings; ++i)
{
emacs_value currentString = env->vec_get(env, args[1], i);
copy_string_contents(env, currentString, &unpackedStrings[i], &stringLengths[i]);
// copy_string_contents() doesn't count the terminator, annoyingly
packedStringsTotal += stringLengths[i] + 1;
}
// Pack strings
char* packedStrings = malloc(packedStringsTotal);
char* currentString = packedStrings;
for (int i = 0; i < numStrings; i++)
{
strcpy(currentString, /*currentString->stringLength,*/ unpackedStrings[i]);
// +1 for null terminator
currentString += stringLengths[i] + 1;
}
free(unpackedStrings);
// Check to see if we're updating the list or adding a new one
FuzzyCachedList* cachedList = NULL;
FuzzyCachedList* emptyListSlot = NULL;
for (int i = 0; i < MAX_CACHED_LISTS; ++i)
{
if (!emptyListSlot && !g_fuzzyCachedLists[i].name)
emptyListSlot = &g_fuzzyCachedLists[i];
if (g_fuzzyCachedLists[i].name && strcmp(nameBuffer, g_fuzzyCachedLists[i].name) == 0)
{
cachedList = &g_fuzzyCachedLists[i];
emptyListSlot = NULL;
break;
}
}
FuzzyCachedList* slotToUse = cachedList ? cachedList : emptyListSlot;
if (cachedList)
{
free(cachedList->packedStrings);
free(cachedList->stringLengths);
free(cachedList->name);
}
// Empty list, copy its name
if (emptyListSlot)
{
slotToUse->name = strdup(nameBuffer);
}
if (slotToUse)
{
slotToUse->packedStrings = packedStrings;
slotToUse->stringLengths = stringLengths;
slotToUse->numStrings = numStrings;
}
else
{
// Reached MAX_CACHED_LISTS; free memory and return failure
free(packedStrings);
free(stringLengths);
return env->make_integer(env, 0);
}
return env->make_integer(env, 1);
}
static emacs_value FmacoyFuzzyFilterCachedList_fun(emacs_env* env, ptrdiff_t nargs,
emacs_value args[], void* data)
{
// Get the cachedListName argument
char* cachedListNameBuffer = NULL;
size_t cachedListNameBufferSize = 0;
copy_string_contents(env, args[0], &cachedListNameBuffer, &cachedListNameBufferSize);
// Get the query argument
char* queryBuffer = NULL;
size_t queryBufferSize = 0;
copy_string_contents(env, args[1], &queryBuffer, &queryBufferSize);
// Get the cached list
FuzzyCachedList* cachedList = NULL;
for (int i = 0; i < MAX_CACHED_LISTS; ++i)
{
if (g_fuzzyCachedLists[i].name &&
strcmp(cachedListNameBuffer, g_fuzzyCachedLists[i].name) == 0)
{
cachedList = &g_fuzzyCachedLists[i];
break;
}
}
// Couldn't find the list by that name or empty query; early-out
if (!cachedList || !queryBuffer || !queryBuffer[0])
return fuzzy_MakeEmptyList(env);
// TODO: Make this resizeable or come up with a way to toss out bad scores?
MacoyFuzzyMatch matches[MAX_MATCHES];
int numMatches = 0;
int numStringsToScore = cachedList->numStrings;
const char* currentString = cachedList->packedStrings;
for (int i = 0; i < numStringsToScore; ++i)
{
const char* stringToCheck = currentString;
int score = 0;
bool isMatch = FUZZY_MATCH(queryBuffer, stringToCheck, &score);
// The string didn't match at all; we won't include it in our results
if (!isMatch)
{
currentString += (cachedList->stringLengths[i] + 1);
continue;
}
// Add the value to our matches
if (numMatches + 1 < MAX_MATCHES)
{
MacoyFuzzyMatch* currentMatch = &matches[numMatches++];
currentMatch->string =
env->make_string(env, stringToCheck, cachedList->stringLengths[i]);
currentMatch->score = score;
currentString += (cachedList->stringLengths[i] + 1);
}
// Reached max number of matches
else
break;
}
return fuzzy_ProcessMatches(env, queryBuffer, matches, numMatches);
}
int emacs_module_init(struct emacs_runtime* ert)
{
emacs_env* env = ert->get_environment(ert);
bind_function(
env, "macoy-fuzzy-cache-list",
env->make_function(env, 2, 2, FmacoyFuzzyCacheVector_fun,
"Cache the vector items in memory under the given name", NULL));
bind_function(
env, "macoy-fuzzy-filter-cached-list",
env->make_function(env, 2, 2, FmacoyFuzzyFilterCachedList_fun,
"Query the cached list which matches the given name. Works like "
"macoy-filter-list-fuzzy, but doesn't support history bonus",
NULL));
bind_function(env, "macoy-filter-list-fuzzy",
env->make_function(env, 2, 2, FmacoyFuzzyFilterVector_fun,
"Filter vector items by query and sort by score.", NULL));
env->make_function(
env, 2, 3, FmacoyFuzzyFilterVector_fun,
"Filter vector items by query and sort by score. The third argument "
"specifies whether the list is ordered by recently selected (history). "
"If the third argument is set to anything, macoy-filter-list-fuzzy will give "
"recently selected items a bonus based on their positions in the list",
NULL));
bind_function(env, "macoy-filter-fuzzy-score",
env->make_function(
env, 2, 2, FmacoyFuzzyScore_fun,
"(query, string). Returns the score of the string based on query", NULL));
provide(env, "macoy-fuzzy");
return 0;
}

105
macoyFuzzy.el

@ -34,7 +34,7 @@
;;; Acknowledgments
;; Forrest Smith wrote the fuzzy matching algorithm.
;; Forrest Smith wrote a fuzzy matching algorithm. I took some inspiration from his implementation
;; flx-ido.el is what I looked at to know how to integrate my module into ido.
;;; Installation:
@ -49,7 +49,6 @@
;;; Code:
;; TODO: Make relative
(when macoy-fuzzy-library-location
(module-load macoy-fuzzy-library-location)
)
@ -57,6 +56,12 @@
(require 'macoy-fuzzy)
;; TODO duplicate and remove
(require 'flx-ido)
(require 'flx)
(defcustom macoy-flx-ido-use-faces t
"Use `flx-highlight-face' to indicate characters contributing to best score."
:type 'boolean
:group 'ido)
(define-minor-mode macoy-fuzzy-ido-mode
"Toggle Macoy fuzzy mode"
@ -65,28 +70,82 @@
:group 'ido
:global t)
;; TODO Remove flx-ido dependency
(defadvice ido-exit-minibuffer (around macoy-fuzzy-ido-reset activate)
"Remove flx properties after."
(let* ((obj (car ido-matches))
(str (if (consp obj)
(car obj)
obj)))
(when (and macoy-fuzzy-ido-mode str)
(remove-text-properties 0 (length str)
'(face flx-highlight-face) str)))
ad-do-it)
;; (defun macoy-flx-ido-decorate (things &optional clear)
;; "Add ido text properties to THINGS.
;; If CLEAR is specified, clear them instead."
;; (if macoy-flx-ido-use-faces
;; (let ((decorate-count (min ido-max-prospects
;; (length things))))
;; (nconc
;; (cl-loop for thing in things
;; for i from 0 below decorate-count
;; collect (if clear
;; (flx-propertize thing nil)
;; (flx-propertize thing 1
;; )
;; ;; (message thing)
;; )
;; )
;; (if clear
;; (nthcdr decorate-count things)
;; (mapcar 'car (nthcdr decorate-count things)))))
;; (if clear
;; things
;; (mapcar 'car things)))
;; )
;; (defun macoy-flx-ido-decorate (things &optional clear)
;; "Add ido text properties to THINGS.
;; If CLEAR is specified, clear them instead."
;; (cl-loop for thing in things
;; for i from 0 below 10
;; do (
;; flx-propertize (car (list thing 1)) 1
;; ;; message thing
;; )
;; )
;; )
;; (defadvice ido-exit-minibuffer (around flx-ido-reset activate)
;; "Remove flx properties after."
;; (let* ((obj (car ido-matches))
;; (str (if (consp obj)
;; (car obj)
;; obj)))
;; (when (and macoy-fuzzy-ido-mode str)
;; (remove-text-properties 0 (length str)
;; '(face flx-highlight-face) str)))
;; ad-do-it)
;; TODO: Sort list by last used? (duplicate whatever behavior the normal stuff does)
(defun macoy-filter-list-fuzzy-ido (query items)
(if (zerop (length query))
(nreverse items) ;; Reverse because the history is in reverse
(macoy-filter-list-fuzzy query (vconcat original-items))
;; By setting t we're telling macoy-filter-list-fuzzy this list
;; is in reverse order of recently selected
;; LEFT OFF: Need to tack on scores for flx-propertize just like in flx-ido-match-internal. Save a copy for the normal return though
(let ((filtered-list
(macoy-filter-list-fuzzy query (vconcat original-items) t)))
;; (let* ((matches (cl-loop for item in filtered-list
;; for string = (ido-name item)
;; collect item
;; into matches
;; finally return matches)))
;; (macoy-flx-ido-decorate matches)
;; )
filtered-list
)
;; TODO duplicate and remove (remember remove properties advice!)
;; (flx-ido-decorate)
)
))
;; When set, use this cached list instead of whatever ido is searching
;; You should set this value, then unset it after the ido search, otherwise every
;; ido-completing-read will look for this value
(setq macoy-fuzzy-use-cache-list-name "")
(defadvice ido-set-matches-1 (around macoy-fuzzy-ido-set-matches-1 activate compile)
"Choose between the regular ido-set-matches-1 and macoy-fuzzy-ido-match"
@ -94,7 +153,13 @@
ad-do-it
(let* ((query ido-text)
(original-items (ad-get-arg 0)))
(setq ad-return-value (macoy-filter-list-fuzzy-ido query original-items)))
(setq ad-return-value
(if macoy-fuzzy-use-cache-list-name
(macoy-fuzzy-filter-cached-list macoy-fuzzy-use-cache-list-name query)
(macoy-filter-list-fuzzy-ido query original-items)
)
)
)
))
(provide 'macoy-fuzzy-ido)

26
macoyFuzzyTests.el

@ -38,3 +38,29 @@
(message "%s" (macoy-filter-fuzzy-score "mac" "emacs.org"))
(message "%s" (macoy-filter-fuzzy-score "mac" "macoyFuzzy.el"))
(macoy-fuzzy-cache-list "testList" ["fdjaf" "test" "balls" "testest"])
(message "%s" (macoy-fuzzy-filter-cached-list "testList" "test"))
(macoy-fuzzy-cache-list "tags" (vconcat macoy-tag-names))
(message "%s" (macoy-fuzzy-filter-cached-list "tags" "test"))
(set macoy-fuzzy-use-cache-list-name "")
(defadvice ido-set-matches-1 (around macoy-fuzzy-ido-set-matches-1 activate compile)
"Choose between the regular ido-set-matches-1 and macoy-fuzzy-ido-match"
(if (not macoy-fuzzy-ido-mode)
ad-do-it
(let* ((query ido-text)
(original-items (ad-get-arg 0)))
(setq ad-return-value
(if macoy-fuzzy-use-cache-list-name
(macoy-fuzzy-filter-cached-list macoy-fuzzy-use-cache-list-name query)
(macoy-filter-list-fuzzy-ido query original-items)
)
)
)
))
(setq macoy-fuzzy-use-cache-list-name "tags")

Loading…
Cancel
Save