Skip to content

Commit 9648d21

Browse files
committed
add Sublime-like fuzzy search to file browser
1 parent dbcecd9 commit 9648d21

File tree

3 files changed

+93
-6
lines changed

3 files changed

+93
-6
lines changed

src/base/base_strings.c

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1863,6 +1863,85 @@ fuzzy_match_range_list_copy(Arena *arena, FuzzyMatchRangeList *src)
18631863
return dst;
18641864
}
18651865

1866+
internal ScoredFuzzyMatchRangeList
1867+
scored_fuzzy_match_find(Arena *arena, String8 needle, String8 haystack)
1868+
{
1869+
Temp scratch = scratch_begin(0, 0);
1870+
// We're going to implement a very simple scoring mechanism similar to that described in
1871+
// https://www.forrestthewoods.com/blog/reverse_engineering_sublime_texts_fuzzy_match/.
1872+
#define scored_fuzzy_match_unmatched -1
1873+
#define scored_fuzzy_match_consecutive 5
1874+
#define scored_fuzzy_match_unmatched_leading -3
1875+
ScoredFuzzyMatchRangeList invalid = {0};
1876+
ScoredFuzzyMatchRangeList result = {0};
1877+
// Simplify to a single needle which has common delimiters removed.
1878+
String8List needles = str8_split(scratch.arena, needle, (U8*)" ", 1, 0);
1879+
needle = str8_list_join(scratch.arena, &needles, 0);
1880+
if (needle.size == 0)
1881+
{
1882+
scratch_end(scratch);
1883+
return invalid;
1884+
}
1885+
String8 tmp_str = str8(needle.str, 1);
1886+
U64 find_pos = 0;
1887+
find_pos = str8_find_needle(haystack, find_pos, tmp_str, StringMatchFlag_CaseInsensitive);
1888+
if (find_pos >= haystack.size)
1889+
{
1890+
scratch_end(scratch);
1891+
return invalid;
1892+
}
1893+
// Leading character penalty.
1894+
// Only go to a max of 3 based on the article.
1895+
result.score += Min(find_pos, 3) * scored_fuzzy_match_unmatched_leading;
1896+
// We also want to deduct for additional unmatched characters between start and find_pos.
1897+
if (find_pos > 3)
1898+
{
1899+
result.score += (find_pos - 3) * scored_fuzzy_match_unmatched;
1900+
}
1901+
Rng1U64 range = r1u64(find_pos, find_pos + 1);
1902+
FuzzyMatchRangeNode *n = push_array(arena, FuzzyMatchRangeNode, 1);
1903+
n->range = range;
1904+
SLLQueuePush(result.list.first, result.list.last, n);
1905+
result.list.count += 1;
1906+
// Match the rest.
1907+
U64 prev_found = find_pos;
1908+
U64 search_start = 0;
1909+
find_pos += 1;
1910+
for (U64 idx = 1; idx < needle.size; ++idx)
1911+
{
1912+
tmp_str = str8(needle.str + idx, 1);
1913+
search_start = find_pos;
1914+
find_pos = str8_find_needle(haystack, find_pos, tmp_str, StringMatchFlag_CaseInsensitive);
1915+
if (find_pos >= haystack.size)
1916+
{
1917+
scratch_end(scratch);
1918+
return invalid;
1919+
}
1920+
// Compute consecutive bonus.
1921+
if (prev_found + 1 == find_pos)
1922+
{
1923+
result.score += scored_fuzzy_match_consecutive;
1924+
// We can reuse the existing node and simply extend it.
1925+
result.list.last->range.max = find_pos + 1;
1926+
}
1927+
else
1928+
{
1929+
result.score += (find_pos - search_start) * scored_fuzzy_match_unmatched;
1930+
Rng1U64 range = r1u64(find_pos, find_pos + 1);
1931+
FuzzyMatchRangeNode *n = push_array(arena, FuzzyMatchRangeNode, 1);
1932+
n->range = range;
1933+
SLLQueuePush(result.list.first, result.list.last, n);
1934+
result.list.count += 1;
1935+
}
1936+
prev_found = find_pos;
1937+
find_pos += 1;
1938+
}
1939+
// Compute final unmatched characters.
1940+
result.score += (haystack.size - find_pos) * scored_fuzzy_match_unmatched;
1941+
scratch_end(scratch);
1942+
return result;
1943+
}
1944+
18661945
////////////////////////////////
18671946
//~ NOTE(allen): Serialization Helpers
18681947

src/base/base_strings.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,13 @@ struct FuzzyMatchRangeList
149149
U64 total_dim;
150150
};
151151

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

@@ -355,6 +362,7 @@ internal Vec4F32 rgba_from_hex_string_4f32(String8 hex_string);
355362

356363
internal FuzzyMatchRangeList fuzzy_match_find(Arena *arena, String8 needle, String8 haystack);
357364
internal FuzzyMatchRangeList fuzzy_match_range_list_copy(Arena *arena, FuzzyMatchRangeList *src);
365+
internal ScoredFuzzyMatchRangeList scored_fuzzy_match_find(Arena *arena, String8 needles, String8 haystack);
358366

359367
////////////////////////////////
360368
//~ NOTE(allen): Serialization Helpers

src/raddbg/raddbg_views.c

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3874,7 +3874,7 @@ struct RD_FileInfo
38743874
{
38753875
String8 filename;
38763876
FileProperties props;
3877-
FuzzyMatchRangeList match_ranges;
3877+
ScoredFuzzyMatchRangeList match_ranges;
38783878
};
38793879

38803880
typedef struct RD_FileInfoNode RD_FileInfoNode;
@@ -3999,11 +3999,11 @@ internal int
39993999
rd_qsort_compare_file_info__default_filtered(RD_FileInfo *a, RD_FileInfo *b)
40004000
{
40014001
int result = 0;
4002-
if(a->filename.size < b->filename.size)
4002+
if(a->match_ranges.score > b->match_ranges.score)
40034003
{
40044004
result = -1;
40054005
}
4006-
else if(a->filename.size > b->filename.size)
4006+
if(a->match_ranges.score < b->match_ranges.score)
40074007
{
40084008
result = +1;
40094009
}
@@ -4109,8 +4109,8 @@ RD_VIEW_RULE_UI_FUNCTION_DEF(file_system)
41094109
OS_FileIter *it = os_file_iter_begin(scratch.arena, path_query.path, 0);
41104110
for(OS_FileInfo info = {0}; os_file_iter_next(scratch.arena, it, &info);)
41114111
{
4112-
FuzzyMatchRangeList match_ranges = fuzzy_match_find(fs->cached_files_arena, path_query.search, info.name);
4113-
B32 fits_search = (path_query.search.size == 0 || match_ranges.count == match_ranges.needle_part_count);
4112+
ScoredFuzzyMatchRangeList match_ranges = scored_fuzzy_match_find(fs->cached_files_arena, path_query.search, info.name);
4113+
B32 fits_search = (path_query.search.size == 0 || match_ranges.list.count != 0);
41144114
B32 fits_dir_only = !!(info.props.flags & FilePropertyFlag_IsFolder) || !dir_selection;
41154115
if(fits_search && fits_dir_only)
41164116
{
@@ -4392,7 +4392,7 @@ RD_VIEW_RULE_UI_FUNCTION_DEF(file_system)
43924392
UI_PrefWidth(ui_pct(1, 0))
43934393
{
43944394
UI_Box *box = ui_build_box_from_string(UI_BoxFlag_DrawText|UI_BoxFlag_DisableIDString, file->filename);
4395-
ui_box_equip_fuzzy_match_ranges(box, &file->match_ranges);
4395+
ui_box_equip_fuzzy_match_ranges(box, &file->match_ranges.list);
43964396
}
43974397
}
43984398

0 commit comments

Comments
 (0)