-
Notifications
You must be signed in to change notification settings - Fork 6
/
fuzzy_history.lua
244 lines (220 loc) · 8.82 KB
/
fuzzy_history.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
--------------------------------------------------------------------------------
-- Fuzzy history suggestion strategy.
--
-- This is like the "history" strategy, but it does fuzzy matching on the
-- command word, instead of being requiring a literal prefix match.
--
-- To use this, include "fuzzy_history" in the autosuggest.strategy setting.
-- Use "clink set autosuggest.strategy <list of strategies>" to set it.
-- See "clink set autosuggest.strategy" or the Clink documentation for more
-- information about suggestions and the autosuggest.strategy setting.
--
-- Settings:
--
-- fuzzy_history.max_items
-- This limits how many history entries are searched.
--
-- fuzzy_history.max_time
-- This limits how many milliseconds can be spent searching
-- history entries. On my laptop, 5000 entries take under 5
-- milliseconds to search.
--
-- fuzzy_history.ignore_path
-- The fuzzy match can ignore the path component of the command
-- word ("hello.exe" matches "\foo\hello.exe").
--
-- fuzzy_history.ignore_ext
-- The fuzzy match can ignore the file extension of the command
-- word ("hello" matches "hello.exe").
if not rl.gethistorycount then
print("fuzzy_history.lua requires a newer version of Clink; please upgrade.")
return
end
settings.add('fuzzy_history.max_items', 5000, 'Limit fuzzy_history searches',
'The fuzzy_history suggestion strategy can search up to this many history\n'..
'entries. If not set or set to 0, it is unlimited.')
settings.add('fuzzy_history.max_time', 25, 'Limit fuzzy_history searches',
'The fuzzy_history suggestion strategy can spend up to this many milliseconds\n'..
'to search history entries. If not set or set to 0, it is unlimited.')
settings.add('fuzzy_history.ignore_path', true, 'Ignores command path',
'When true, the fuzzy_history suggestion strategy ignores command paths when\n'..
'searching history. E.g. "\\foo\\bar\\hello" matches "hello".')
settings.add('fuzzy_history.ignore_ext', true, 'Ignores command extension',
'When true, the fuzzy_history suggestion strategy ignores command file\n'..
'extensions when searching history. E.g. "hello.exe" matches "hello".')
--------------------------------------------------------------------------------
local function log_if_expensive(tick, found, count)
-- Anything over about 100 ms is noticable.
-- Typical search time for 5000 items on my laptop is under 5 ms.
local elapsed = os.clock() - tick
if elapsed > 0.200 then
local msg = 'PERFORMANCE: fuzzy_history took '..elapsed..' sec; '..count..' history entries searched; '
if found then
msg = msg..'match found'
else
msg = msg..'no matches found'
end
log.info(msg)
end
end
--------------------------------------------------------------------------------
local function transform_command(command, ignore_path, ignore_ext)
if ignore_path then
if ignore_ext then
return path.getbasename(command), path.getextension(command)
else
return path.getname(command), ''
end
else
local p = path.getdirectory(command)
if ignore_ext then
return path.join(p, path.getbasename(command)), path.getextension(command)
else
return path.join(p, path.getname(command)), ''
end
end
end
--------------------------------------------------------------------------------
local function test_ending_dot_mismatch(needle, haystack)
if needle:sub(-1) == "." then
haystack = path.getname(haystack)
local ml = string.matchlen(needle, haystack)
if ml >= 0 and ml ~= #needle then
needle = needle:gsub("%.+$", "")
haystack = haystack:gsub("%.+$", "")
return string.matchlen(needle, haystack) >= 0
end
end
end
--------------------------------------------------------------------------------
local sug = clink.suggester('fuzzy_history')
--------------------------------------------------------------------------------
function sug:suggest(line_state, matches) -- luacheck: no unused
-- If empty or only spaces there's nothing to match.
local line = line_state:getline()
if not line:match('[^ ]') then
return
end
-- If more than 2 words it's definitely not a match.
if line_state:getwordcount() > 2 then
return
end
-- First word must not be redirection.
local info = line_state:getwordinfo(1)
if not info or info.redir then
return
end
-- Get command word.
local command, gap
local endquote
if line_state:getwordcount() == 1 then
command = line:sub(info.offset, line_state:getcursor() - 1)
if info.quoted and command:sub(-1) == '"' then
command = command:sub(1, #command - 1)
endquote = true
end
gap = ''
else
command = line_state:getword(1)
gap = line:sub(info.offset + info.length, line_state:getcursor() - 1)
-- Must be one word followed by optional whitespace; no second word.
if info.quoted and gap:find('^"') then
gap = gap:sub(2)
endquote = true
end
if gap:gsub(' ', '') ~= '' then
return
end
end
if #command == 0 then
return
end
local ignore_path = settings.get('fuzzy_history.ignore_path')
local ignore_ext = settings.get('fuzzy_history.ignore_ext')
local name, ext = transform_command(command, ignore_path, ignore_ext)
local nameext = path.getname(command)
if not name or name == '' then
return
end
local max_items = settings.get('fuzzy_history.max_items')
local upperbound = rl.gethistorycount()
local lowerbound = 1
if max_items and max_items > 0 then
lowerbound = upperbound + 1 - max_items
if lowerbound < 1 then
lowerbound = 1
end
end
local max_time = settings.get('fuzzy_history.max_time')
if max_time and max_time <= 0 then
max_time = nil
else
max_time = max_time / 1000
end
local tick = os.clock()
local batch = 0
for i = upperbound, lowerbound, -1 do
local h = rl.gethistoryitems(i, i)[1]
local q = 0
local s, e = h.line:find('^ *"([^"]+)"')
if s then
q = 1
else
s, e = h.line:find('^ *([^" ][^ ]*)')
end
if s then
local hc = h.line:sub(s, e)
local hn, he = transform_command(hc, ignore_path, ignore_ext)
local both, hb
-- Try for an exact match of the base name, plus either an exact
-- match of the extension or an omitted extension.
local partial = 0
local found = (string.matchlen(name, hn) < 0 and
(he == '' or ext == '' or string.matchlen(ext, he) < 0))
-- If no match and there isn't a space yet after the command word,
-- then try for a prefix match.
if not found and gap == '' and hn ~= '' then
both = name..ext
hb = hn..he
local len = string.matchlen(both, hb)
if not endquote and len == #both and not test_ending_dot_mismatch(nameext, hc) then
found = true
partial = #hb - string.matchlen(hb, both)
end
end
-- If the command word matches, then use the rest of the history
-- line as the suggestion.
if found then
-- Take the rest of the history line, but strip leading spaces.
-- If that's not empty, then we have a winner.
local suggestion = h.line:sub(e + 1 + q):gsub('^( +)', '')
if suggestion ~= '' then
if gap == '' then
suggestion = ' '..suggestion
if partial > 0 then
hb = hb:sub(0 - partial):gsub('"+$', '')
endquote = false
end
if info.quoted and not endquote then
suggestion = '"'..suggestion
end
if partial > 0 then
suggestion = hb..suggestion
end
end
suggestion = line:sub(1, line_state:getcursor() - 1)..suggestion
log_if_expensive(tick, true, upperbound + 1 - i)
return suggestion, 1
end
end
end
batch = batch + 1
if batch >= 5 then
if os.clock() - tick > max_time then
break
end
batch = 0
end
end
log_if_expensive(tick, false, upperbound + 1 - lowerbound)
end