Skip to content

Commit 9cefed9

Browse files
committed
add Sublime-like fuzzy search to file browser
1 parent 21deaac commit 9cefed9

File tree

5 files changed

+95
-10
lines changed

5 files changed

+95
-10
lines changed

src/df/core/df_core.h

+7
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@ struct DF_FuzzyMatchRangeList
8888
U64 count;
8989
};
9090

91+
typedef struct DF_ScoredFuzzyMatchRangeList DF_ScoredFuzzyMatchRangeList;
92+
struct DF_ScoredFuzzyMatchRangeList
93+
{
94+
DF_FuzzyMatchRangeList list;
95+
S32 score;
96+
};
97+
9198
////////////////////////////////
9299
//~ rjf: Control Context Types
93100

src/df/gfx/df_gfx.c

+78
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,84 @@ df_fuzzy_match_find(Arena *arena, String8List needles, String8 haystack)
102102
return result;
103103
}
104104

105+
internal DF_ScoredFuzzyMatchRangeList
106+
df_scored_fuzzy_match_find(Arena *arena, String8List needles, String8 haystack)
107+
{
108+
Temp scratch = scratch_begin(0, 0);
109+
// We're going to implement a very simple scoring mechanism similar to that described in
110+
// https://www.forrestthewoods.com/blog/reverse_engineering_sublime_texts_fuzzy_match/.
111+
#define df_scored_unmatched -1
112+
#define df_scored_consecutive 5
113+
#define df_scored_unmatched_leading -3
114+
DF_ScoredFuzzyMatchRangeList invalid = {0};
115+
DF_ScoredFuzzyMatchRangeList result = {0};
116+
// Simplify to a single needle.
117+
String8 needle = str8_list_join(scratch.arena, &needles, 0);
118+
if (needle.size == 0)
119+
{
120+
scratch_end(scratch);
121+
return invalid;
122+
}
123+
String8 tmp_str = str8(needle.str, 1);
124+
U64 find_pos = 0;
125+
find_pos = str8_find_needle(haystack, find_pos, tmp_str, StringMatchFlag_CaseInsensitive);
126+
if (find_pos >= haystack.size)
127+
{
128+
scratch_end(scratch);
129+
return invalid;
130+
}
131+
// Leading character penalty.
132+
// Only go to a max of 3 based on the article.
133+
result.score += Min(find_pos, 3) * df_scored_unmatched_leading;
134+
// We also want to deduct for additional unmatched characters between start and find_pos.
135+
if (find_pos > 3)
136+
{
137+
result.score += (find_pos - 3) * df_scored_unmatched;
138+
}
139+
Rng1U64 range = r1u64(find_pos, find_pos + 1);
140+
DF_FuzzyMatchRangeNode *n = push_array(arena, DF_FuzzyMatchRangeNode, 1);
141+
n->range = range;
142+
SLLQueuePush(result.list.first, result.list.last, n);
143+
result.list.count += 1;
144+
// Match the rest.
145+
U64 prev_found = find_pos;
146+
U64 search_start = 0;
147+
find_pos += 1;
148+
for (U64 idx = 1; idx < needle.size; ++idx)
149+
{
150+
tmp_str = str8(needle.str + idx, 1);
151+
search_start = find_pos;
152+
find_pos = str8_find_needle(haystack, find_pos, tmp_str, StringMatchFlag_CaseInsensitive);
153+
if (find_pos >= haystack.size)
154+
{
155+
scratch_end(scratch);
156+
return invalid;
157+
}
158+
// Compute consecutive bonus.
159+
if (prev_found + 1 == find_pos)
160+
{
161+
result.score += df_scored_consecutive;
162+
// We can reuse the existing node and simply extend it.
163+
result.list.last->range.max = find_pos + 1;
164+
}
165+
else
166+
{
167+
result.score += (find_pos - search_start) * df_scored_unmatched;
168+
Rng1U64 range = r1u64(find_pos, find_pos + 1);
169+
DF_FuzzyMatchRangeNode *n = push_array(arena, DF_FuzzyMatchRangeNode, 1);
170+
n->range = range;
171+
SLLQueuePush(result.list.first, result.list.last, n);
172+
result.list.count += 1;
173+
}
174+
prev_found = find_pos;
175+
find_pos += 1;
176+
}
177+
// Compute final unmatched characters.
178+
result.score += (haystack.size - find_pos) * df_scored_unmatched;
179+
scratch_end(scratch);
180+
return result;
181+
}
182+
105183
////////////////////////////////
106184
//~ rjf: View Type Functions
107185

src/df/gfx/df_gfx.h

+1
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,7 @@ global DF_DragDropPayload df_g_drag_drop_payload = {0};
803803

804804
internal DF_PathQuery df_path_query_from_string(String8 string);
805805
internal DF_FuzzyMatchRangeList df_fuzzy_match_find(Arena *arena, String8List needles, String8 haystack);
806+
internal DF_ScoredFuzzyMatchRangeList df_scored_fuzzy_match_find(Arena *arena, String8List needles, String8 haystack);
806807

807808
////////////////////////////////
808809
//~ rjf: View Type Functions

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
@@ -2130,8 +2129,8 @@ DF_VIEW_UI_FUNCTION_DEF(FileSystem)
21302129
OS_FileIter *it = os_file_iter_begin(scratch.arena, path_query.path, 0);
21312130
for(OS_FileInfo info = {0}; os_file_iter_next(scratch.arena, it, &info);)
21322131
{
2133-
DF_FuzzyMatchRangeList match_ranges = df_fuzzy_match_find(fs->cached_files_arena, search_needles, info.name);
2134-
B32 fits_search = (search_needles.node_count == 0 || match_ranges.count == search_needles.node_count);
2132+
DF_ScoredFuzzyMatchRangeList match_ranges = df_scored_fuzzy_match_find(fs->cached_files_arena, search_needles, info.name);
2133+
B32 fits_search = (search_needles.node_count == 0 || match_ranges.list.count != 0);
21352134
B32 fits_dir_only = !!(info.props.flags & FilePropertyFlag_IsFolder) || !dir_selection;
21362135
if(fits_search && fits_dir_only)
21372136
{
@@ -2430,7 +2429,7 @@ DF_VIEW_UI_FUNCTION_DEF(FileSystem)
24302429
UI_PrefWidth(ui_pct(1, 0))
24312430
{
24322431
UI_Box *box = ui_build_box_from_stringf(UI_BoxFlag_DrawText, "%S##%p", file->filename, view);
2433-
df_box_equip_fuzzy_match_range_list_vis(box, file->match_ranges);
2432+
df_box_equip_fuzzy_match_range_list_vis(box, file->match_ranges.list);
24342433
}
24352434
}
24362435

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-
DF_FuzzyMatchRangeList match_ranges;
25+
DF_ScoredFuzzyMatchRangeList match_ranges;
2626
};
2727

2828
typedef struct DF_FileInfoNode DF_FileInfoNode;

0 commit comments

Comments
 (0)