fuzzy file finder
haldean
2 years ago
0 | import algorithm | |
1 | import options | |
2 | import os | |
3 | import re | |
4 | import sets | |
5 | import sequtils | |
6 | ||
7 | type | |
8 | Found = seq[string] | |
9 | Score = (float, float) | |
10 | ||
11 | let exclude = toHashSet([".git", "gen", "inst"]) | |
12 | ||
13 | proc includef(fname: string): bool = | |
14 | var x = fname | |
15 | while true: | |
16 | let (dir, base) = splitPath(x) | |
17 | if exclude.contains(base): | |
18 | return false | |
19 | if base[0] == '.': | |
20 | return false | |
21 | if dir == "/" or dir == "": | |
22 | return true | |
23 | x = dir | |
24 | ||
25 | proc find*(root: string): Found = | |
26 | var found = newSeq[string]() | |
27 | for file in walkDirRec(root): | |
28 | if includef(file): | |
29 | found.add(file) | |
30 | return found | |
31 | ||
32 | proc lcss(s1, s2: string): int = | |
33 | let | |
34 | m1 = s1.len - 1 | |
35 | m2 = s2.len - 1 | |
36 | var | |
37 | cache = newSeq[seq[int]]() | |
38 | maxlen = 0 | |
39 | ||
40 | for i in 0..m1: | |
41 | var s = newSeq[int]() | |
42 | for i in 0..m2: | |
43 | s.add(0) | |
44 | cache.add(s) | |
45 | ||
46 | for i1 in 0..m1: | |
47 | for i2 in 0..m2: | |
48 | if s1[i1] == s2[i2]: | |
49 | var x = 0 | |
50 | if i1 == 0 or i2 == 0: | |
51 | x = 1 | |
52 | else: | |
53 | x = cache[i1-1][i2-1] + 1 | |
54 | cache[i1][i2] = x | |
55 | if x > maxlen: | |
56 | maxlen = x | |
57 | return maxlen | |
58 | ||
59 | proc score(path, search: string): Score = | |
60 | let | |
61 | (_, base) = splitPath(path) | |
62 | basescore = lcss(search, base).float | |
63 | fullscore = lcss(search, path).float | |
64 | # In the event of a tie, break the tie by which has the longest match in the | |
65 | # basename of the file. Negated so that lower scores are better, so we can | |
66 | # ascending-sort on score and file name | |
67 | return (-fullscore, -basescore) | |
68 | ||
69 | proc find*(root, search: string): Found = | |
70 | var patstr = "" | |
71 | for ch in search: | |
72 | patstr = patstr & ".*" & ch | |
73 | patstr = patstr & ".*" | |
74 | let pat = re(patstr, {reIgnoreCase}) | |
75 | ||
76 | let found = find(root).filter do (abspath: string) -> bool: | |
77 | return match(relativePath(abspath, root), pat) | |
78 | ||
79 | var scored = found.map do (abspath: string) -> (Score, string): | |
80 | let relpath = relativePath(abspath, root) | |
81 | return (score(relpath, search), relpath) | |
82 | ||
83 | scored.sort() | |
84 | return scored.map do (p: (Score, string)) -> string: p[1] | |
85 | ||
86 | when isMainModule: | |
87 | var found: Found | |
88 | if paramCount() < 1: | |
89 | found = find(getCurrentDir()) | |
90 | else: | |
91 | let search = paramstr(1) | |
92 | found = find(getCurrentDir(), search) | |
93 | for f in found: | |
94 | echo f |