Skip to content

add Sublime-like fuzzy search to file browser #94

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions src/base/base_strings.c
Original file line number Diff line number Diff line change
Expand Up @@ -1863,6 +1863,85 @@ fuzzy_match_range_list_copy(Arena *arena, FuzzyMatchRangeList *src)
return dst;
}

internal ScoredFuzzyMatchRangeList
scored_fuzzy_match_find(Arena *arena, String8 needle, String8 haystack)
{
Temp scratch = scratch_begin(0, 0);
// We're going to implement a very simple scoring mechanism similar to that described in
// https://www.forrestthewoods.com/blog/reverse_engineering_sublime_texts_fuzzy_match/.
#define scored_fuzzy_match_unmatched -1
#define scored_fuzzy_match_consecutive 5
#define scored_fuzzy_match_unmatched_leading -3
ScoredFuzzyMatchRangeList invalid = {0};
ScoredFuzzyMatchRangeList result = {0};
// Simplify to a single needle which has common delimiters removed.
String8List needles = str8_split(scratch.arena, needle, (U8*)" ", 1, 0);
needle = str8_list_join(scratch.arena, &needles, 0);
if (needle.size == 0)
{
scratch_end(scratch);
return invalid;
}
String8 tmp_str = str8(needle.str, 1);
U64 find_pos = 0;
find_pos = str8_find_needle(haystack, find_pos, tmp_str, StringMatchFlag_CaseInsensitive);
if (find_pos >= haystack.size)
{
scratch_end(scratch);
return invalid;
}
// Leading character penalty.
// Only go to a max of 3 based on the article.
result.score += Min(find_pos, 3) * scored_fuzzy_match_unmatched_leading;
// We also want to deduct for additional unmatched characters between start and find_pos.
if (find_pos > 3)
{
result.score += (find_pos - 3) * scored_fuzzy_match_unmatched;
}
Rng1U64 range = r1u64(find_pos, find_pos + 1);
FuzzyMatchRangeNode *n = push_array(arena, FuzzyMatchRangeNode, 1);
n->range = range;
SLLQueuePush(result.list.first, result.list.last, n);
result.list.count += 1;
// Match the rest.
U64 prev_found = find_pos;
U64 search_start = 0;
find_pos += 1;
for (U64 idx = 1; idx < needle.size; ++idx)
{
tmp_str = str8(needle.str + idx, 1);
search_start = find_pos;
find_pos = str8_find_needle(haystack, find_pos, tmp_str, StringMatchFlag_CaseInsensitive);
if (find_pos >= haystack.size)
{
scratch_end(scratch);
return invalid;
}
// Compute consecutive bonus.
if (prev_found + 1 == find_pos)
{
result.score += scored_fuzzy_match_consecutive;
// We can reuse the existing node and simply extend it.
result.list.last->range.max = find_pos + 1;
}
else
{
result.score += (find_pos - search_start) * scored_fuzzy_match_unmatched;
Rng1U64 range = r1u64(find_pos, find_pos + 1);
FuzzyMatchRangeNode *n = push_array(arena, FuzzyMatchRangeNode, 1);
n->range = range;
SLLQueuePush(result.list.first, result.list.last, n);
result.list.count += 1;
}
prev_found = find_pos;
find_pos += 1;
}
// Compute final unmatched characters.
result.score += (haystack.size - find_pos) * scored_fuzzy_match_unmatched;
scratch_end(scratch);
return result;
}

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

Expand Down
8 changes: 8 additions & 0 deletions src/base/base_strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,13 @@ struct FuzzyMatchRangeList
U64 total_dim;
};

typedef struct ScoredFuzzyMatchRangeList ScoredFuzzyMatchRangeList;
struct ScoredFuzzyMatchRangeList
{
FuzzyMatchRangeList list;
S32 score;
};

////////////////////////////////
//~ rjf: Character Classification & Conversion Functions

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

internal FuzzyMatchRangeList fuzzy_match_find(Arena *arena, String8 needle, String8 haystack);
internal FuzzyMatchRangeList fuzzy_match_range_list_copy(Arena *arena, FuzzyMatchRangeList *src);
internal ScoredFuzzyMatchRangeList scored_fuzzy_match_find(Arena *arena, String8 needles, String8 haystack);

////////////////////////////////
//~ NOTE(allen): Serialization Helpers
Expand Down
12 changes: 6 additions & 6 deletions src/raddbg/raddbg_views.c
Original file line number Diff line number Diff line change
Expand Up @@ -3874,7 +3874,7 @@ struct RD_FileInfo
{
String8 filename;
FileProperties props;
FuzzyMatchRangeList match_ranges;
ScoredFuzzyMatchRangeList match_ranges;
};

typedef struct RD_FileInfoNode RD_FileInfoNode;
Expand Down Expand Up @@ -3999,11 +3999,11 @@ internal int
rd_qsort_compare_file_info__default_filtered(RD_FileInfo *a, RD_FileInfo *b)
{
int result = 0;
if(a->filename.size < b->filename.size)
if(a->match_ranges.score > b->match_ranges.score)
{
result = -1;
}
else if(a->filename.size > b->filename.size)
if(a->match_ranges.score < b->match_ranges.score)
{
result = +1;
}
Expand Down Expand Up @@ -4109,8 +4109,8 @@ RD_VIEW_RULE_UI_FUNCTION_DEF(file_system)
OS_FileIter *it = os_file_iter_begin(scratch.arena, path_query.path, 0);
for(OS_FileInfo info = {0}; os_file_iter_next(scratch.arena, it, &info);)
{
FuzzyMatchRangeList match_ranges = fuzzy_match_find(fs->cached_files_arena, path_query.search, info.name);
B32 fits_search = (path_query.search.size == 0 || match_ranges.count == match_ranges.needle_part_count);
ScoredFuzzyMatchRangeList match_ranges = scored_fuzzy_match_find(fs->cached_files_arena, path_query.search, info.name);
B32 fits_search = (path_query.search.size == 0 || match_ranges.list.count != 0);
B32 fits_dir_only = !!(info.props.flags & FilePropertyFlag_IsFolder) || !dir_selection;
if(fits_search && fits_dir_only)
{
Expand Down Expand Up @@ -4392,7 +4392,7 @@ RD_VIEW_RULE_UI_FUNCTION_DEF(file_system)
UI_PrefWidth(ui_pct(1, 0))
{
UI_Box *box = ui_build_box_from_string(UI_BoxFlag_DrawText|UI_BoxFlag_DisableIDString, file->filename);
ui_box_equip_fuzzy_match_ranges(box, &file->match_ranges);
ui_box_equip_fuzzy_match_ranges(box, &file->match_ranges.list);
}
}

Expand Down