Skip to content

Commit 5c66290

Browse files
committed
add Sublime-like fuzzy search to file browser
1 parent ba45dd1 commit 5c66290

File tree

4 files changed

+96
-10
lines changed

4 files changed

+96
-10
lines changed

src/base/base_strings.c

+79
Original file line numberDiff line numberDiff line change
@@ -1754,6 +1754,85 @@ fuzzy_match_range_list_copy(Arena *arena, FuzzyMatchRangeList *src)
17541754
return dst;
17551755
}
17561756

1757+
internal ScoredFuzzyMatchRangeList
1758+
scored_fuzzy_match_find(Arena *arena, String8 needle, String8 haystack)
1759+
{
1760+
Temp scratch = scratch_begin(0, 0);
1761+
// We're going to implement a very simple scoring mechanism similar to that described in
1762+
// https://www.forrestthewoods.com/blog/reverse_engineering_sublime_texts_fuzzy_match/.
1763+
#define scored_fuzzy_match_unmatched -1
1764+
#define scored_fuzzy_match_consecutive 5
1765+
#define scored_fuzzy_match_unmatched_leading -3
1766+
ScoredFuzzyMatchRangeList invalid = {0};
1767+
ScoredFuzzyMatchRangeList result = {0};
1768+
// Simplify to a single needle which has common delimiters removed.
1769+
String8List needles = str8_split(scratch.arena, needle, (U8*)" ", 1, 0);
1770+
needle = str8_list_join(scratch.arena, &needles, 0);
1771+
if (needle.size == 0)
1772+
{
1773+
scratch_end(scratch);
1774+
return invalid;
1775+
}
1776+
String8 tmp_str = str8(needle.str, 1);
1777+
U64 find_pos = 0;
1778+
find_pos = str8_find_needle(haystack, find_pos, tmp_str, StringMatchFlag_CaseInsensitive);
1779+
if (find_pos >= haystack.size)
1780+
{
1781+
scratch_end(scratch);
1782+
return invalid;
1783+
}
1784+
// Leading character penalty.
1785+
// Only go to a max of 3 based on the article.
1786+
result.score += Min(find_pos, 3) * scored_fuzzy_match_unmatched_leading;
1787+
// We also want to deduct for additional unmatched characters between start and find_pos.
1788+
if (find_pos > 3)
1789+
{
1790+
result.score += (find_pos - 3) * scored_fuzzy_match_unmatched;
1791+
}
1792+
Rng1U64 range = r1u64(find_pos, find_pos + 1);
1793+
FuzzyMatchRangeNode *n = push_array(arena, FuzzyMatchRangeNode, 1);
1794+
n->range = range;
1795+
SLLQueuePush(result.list.first, result.list.last, n);
1796+
result.list.count += 1;
1797+
// Match the rest.
1798+
U64 prev_found = find_pos;
1799+
U64 search_start = 0;
1800+
find_pos += 1;
1801+
for (U64 idx = 1; idx < needle.size; ++idx)
1802+
{
1803+
tmp_str = str8(needle.str + idx, 1);
1804+
search_start = find_pos;
1805+
find_pos = str8_find_needle(haystack, find_pos, tmp_str, StringMatchFlag_CaseInsensitive);
1806+
if (find_pos >= haystack.size)
1807+
{
1808+
scratch_end(scratch);
1809+
return invalid;
1810+
}
1811+
// Compute consecutive bonus.
1812+
if (prev_found + 1 == find_pos)
1813+
{
1814+
result.score += scored_fuzzy_match_consecutive;
1815+
// We can reuse the existing node and simply extend it.
1816+
result.list.last->range.max = find_pos + 1;
1817+
}
1818+
else
1819+
{
1820+
result.score += (find_pos - search_start) * scored_fuzzy_match_unmatched;
1821+
Rng1U64 range = r1u64(find_pos, find_pos + 1);
1822+
FuzzyMatchRangeNode *n = push_array(arena, FuzzyMatchRangeNode, 1);
1823+
n->range = range;
1824+
SLLQueuePush(result.list.first, result.list.last, n);
1825+
result.list.count += 1;
1826+
}
1827+
prev_found = find_pos;
1828+
find_pos += 1;
1829+
}
1830+
// Compute final unmatched characters.
1831+
result.score += (haystack.size - find_pos) * scored_fuzzy_match_unmatched;
1832+
scratch_end(scratch);
1833+
return result;
1834+
}
1835+
17571836
////////////////////////////////
17581837
//~ NOTE(allen): Serialization Helpers
17591838

src/base/base_strings.h

+8
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,13 @@ struct FuzzyMatchRangeList
148148
U64 total_dim;
149149
};
150150

151+
typedef struct ScoredFuzzyMatchRangeList ScoredFuzzyMatchRangeList;
152+
struct ScoredFuzzyMatchRangeList
153+
{
154+
FuzzyMatchRangeList list;
155+
S32 score;
156+
};
157+
151158
////////////////////////////////
152159
//~ rjf: Character Classification & Conversion Functions
153160

@@ -347,6 +354,7 @@ internal Vec4F32 rgba_from_hex_string_4f32(String8 hex_string);
347354

348355
internal FuzzyMatchRangeList fuzzy_match_find(Arena *arena, String8 needle, String8 haystack);
349356
internal FuzzyMatchRangeList fuzzy_match_range_list_copy(Arena *arena, FuzzyMatchRangeList *src);
357+
internal ScoredFuzzyMatchRangeList scored_fuzzy_match_find(Arena *arena, String8 needles, String8 haystack);
350358

351359
////////////////////////////////
352360
//~ NOTE(allen): Serialization Helpers

src/df/gfx/df_views.c

+8-9
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,15 @@ df_qsort_compare_file_info__default(DF_FileInfo *a, DF_FileInfo *b)
2626
internal int
2727
df_qsort_compare_file_info__default_filtered(DF_FileInfo *a, DF_FileInfo *b)
2828
{
29-
int result = 0;
30-
if(a->filename.size < b->filename.size)
29+
if (a->match_ranges.score > b->match_ranges.score)
3130
{
32-
result = -1;
31+
return -1;
3332
}
34-
else if(a->filename.size > b->filename.size)
33+
if (a->match_ranges.score < b->match_ranges.score)
3534
{
36-
result = +1;
35+
return 1;
3736
}
38-
return result;
37+
return 0;
3938
}
4039

4140
internal int
@@ -3399,8 +3398,8 @@ DF_VIEW_UI_FUNCTION_DEF(FileSystem)
33993398
OS_FileIter *it = os_file_iter_begin(scratch.arena, path_query.path, 0);
34003399
for(OS_FileInfo info = {0}; os_file_iter_next(scratch.arena, it, &info);)
34013400
{
3402-
FuzzyMatchRangeList match_ranges = fuzzy_match_find(fs->cached_files_arena, path_query.search, info.name);
3403-
B32 fits_search = (path_query.search.size == 0 || match_ranges.count == match_ranges.needle_part_count);
3401+
ScoredFuzzyMatchRangeList match_ranges = scored_fuzzy_match_find(fs->cached_files_arena, path_query.search, info.name);
3402+
B32 fits_search = (path_query.search.size == 0 || match_ranges.list.count != 0);
34043403
B32 fits_dir_only = !!(info.props.flags & FilePropertyFlag_IsFolder) || !dir_selection;
34053404
if(fits_search && fits_dir_only)
34063405
{
@@ -3696,7 +3695,7 @@ DF_VIEW_UI_FUNCTION_DEF(FileSystem)
36963695
UI_PrefWidth(ui_pct(1, 0))
36973696
{
36983697
UI_Box *box = ui_build_box_from_string(UI_BoxFlag_DrawText|UI_BoxFlag_DisableIDString, file->filename);
3699-
ui_box_equip_fuzzy_match_ranges(box, &file->match_ranges);
3698+
ui_box_equip_fuzzy_match_ranges(box, &file->match_ranges.list);
37003699
}
37013700
}
37023701

src/df/gfx/df_views.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ struct DF_FileInfo
2222
{
2323
String8 filename;
2424
FileProperties props;
25-
FuzzyMatchRangeList match_ranges;
25+
ScoredFuzzyMatchRangeList match_ranges;
2626
};
2727

2828
typedef struct DF_FileInfoNode DF_FileInfoNode;

0 commit comments

Comments
 (0)