Skip to content

Commit 790695e

Browse files
committed
add Sublime-like fuzzy search to file browser
1 parent 2e13b22 commit 790695e

File tree

3 files changed

+93
-6
lines changed

3 files changed

+93
-6
lines changed

src/base/base_strings.c

+79
Original file line numberDiff line numberDiff line change
@@ -1855,6 +1855,85 @@ fuzzy_match_range_list_copy(Arena *arena, FuzzyMatchRangeList *src)
18551855
return dst;
18561856
}
18571857

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

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

@@ -353,6 +360,7 @@ internal Vec4F32 rgba_from_hex_string_4f32(String8 hex_string);
353360

354361
internal FuzzyMatchRangeList fuzzy_match_find(Arena *arena, String8 needle, String8 haystack);
355362
internal FuzzyMatchRangeList fuzzy_match_range_list_copy(Arena *arena, FuzzyMatchRangeList *src);
363+
internal ScoredFuzzyMatchRangeList scored_fuzzy_match_find(Arena *arena, String8 needles, String8 haystack);
356364

357365
////////////////////////////////
358366
//~ NOTE(allen): Serialization Helpers

src/raddbg/raddbg_views.c

+6-6
Original file line numberDiff line numberDiff line change
@@ -3594,7 +3594,7 @@ struct RD_FileInfo
35943594
{
35953595
String8 filename;
35963596
FileProperties props;
3597-
FuzzyMatchRangeList match_ranges;
3597+
ScoredFuzzyMatchRangeList match_ranges;
35983598
};
35993599

36003600
typedef struct RD_FileInfoNode RD_FileInfoNode;
@@ -3719,11 +3719,11 @@ internal int
37193719
rd_qsort_compare_file_info__default_filtered(RD_FileInfo *a, RD_FileInfo *b)
37203720
{
37213721
int result = 0;
3722-
if(a->filename.size < b->filename.size)
3722+
if(a->match_ranges.score > b->match_ranges.score)
37233723
{
37243724
result = -1;
37253725
}
3726-
else if(a->filename.size > b->filename.size)
3726+
if(a->match_ranges.score < b->match_ranges.score)
37273727
{
37283728
result = +1;
37293729
}
@@ -3829,8 +3829,8 @@ RD_VIEW_RULE_UI_FUNCTION_DEF(file_system)
38293829
OS_FileIter *it = os_file_iter_begin(scratch.arena, path_query.path, 0);
38303830
for(OS_FileInfo info = {0}; os_file_iter_next(scratch.arena, it, &info);)
38313831
{
3832-
FuzzyMatchRangeList match_ranges = fuzzy_match_find(fs->cached_files_arena, path_query.search, info.name);
3833-
B32 fits_search = (path_query.search.size == 0 || match_ranges.count == match_ranges.needle_part_count);
3832+
ScoredFuzzyMatchRangeList match_ranges = scored_fuzzy_match_find(fs->cached_files_arena, path_query.search, info.name);
3833+
B32 fits_search = (path_query.search.size == 0 || match_ranges.list.count != 0);
38343834
B32 fits_dir_only = !!(info.props.flags & FilePropertyFlag_IsFolder) || !dir_selection;
38353835
if(fits_search && fits_dir_only)
38363836
{
@@ -4112,7 +4112,7 @@ RD_VIEW_RULE_UI_FUNCTION_DEF(file_system)
41124112
UI_PrefWidth(ui_pct(1, 0))
41134113
{
41144114
UI_Box *box = ui_build_box_from_string(UI_BoxFlag_DrawText|UI_BoxFlag_DisableIDString, file->filename);
4115-
ui_box_equip_fuzzy_match_ranges(box, &file->match_ranges);
4115+
ui_box_equip_fuzzy_match_ranges(box, &file->match_ranges.list);
41164116
}
41174117
}
41184118

0 commit comments

Comments
 (0)