-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathvi_find_files.lua
328 lines (302 loc) · 10.4 KB
/
vi_find_files.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
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
-- Search for file by name/pattern.
local M = {}
local DEBUG_FIND_FILES=false
local debug_find
local debug_find_files_file
local debug_ts
if DEBUG_FIND_FILES then
debug_ts = require('textadept-vi.vi_util').tostring
debug_find = function(text)
if debug_find_files_file == nil then
debug_find_files_file = io.open("ta_debug_find_files.txt", "w")
end
debug_find_files_file:write(text .. "\n")
debug_find_files_file:flush()
end
else
debug_ts = function() return "" end
debug_find = function() end
end
local lfs = _G.lfs
local vi_regex = require('textadept-vi.regex.pegex')
-- Escape a Lua pattern to make it an exact match.
function M.luapat_escape(s)
-- replace metacharacters
s = s:gsub("[%(%)%%%.%[%]%*%+%-%?]", function (s) return "%"..s end)
-- ^ and $ only apply at the start/end
if s:sub(1,1) == "^" then s = "%" .. s end
if s:sub(-1,-1) == "$" then s = s:sub(1,-2) .. "%$" end
return s
end
-- Escape a glob pattern to make it an exact match.
function M.glob_escape(s)
-- replace metacharacters
local s = s:gsub("[][\\*?]", function (s) return "\\"..s end)
return s
end
local function mkmatch_luapat(pat, allow_wild_end)
local fullpat = '^' .. pat
if allow_wild_end then
fullpat = fullpat .. '.*'
end
fullpat = fullpat .. '$'
return function(text)
local result = text:match(fullpat)
return result
end
end
-- Convert a glob pattern into a Lua pattern
local function glob_to_luapat(pat)
local tab = {'^'}
local i=1
while i <= #pat do
local c = pat:sub(i, i)
if c == "*" then
table.insert(tab, ".*")
elseif c == "?" then
table.insert(tab, ".")
elseif c:match("[%^%$%(%)%%%.%[%]%+%-]") then
table.insert(tab, "%")
table.insert(tab, c)
-- elseif handle character class
else
table.insert(tab, c)
end
i = i + 1
end
return table.concat(tab)
end
local function mkmatch_glob(pat, allow_wild_end)
-- Convert a glob pattern into a Lua pattern
local fullpat = glob_to_luapat(pat)
if allow_wild_end then
-- Special case for empty pattern - don't match dotfiles
if fullpat == "^" then
fullpat = fullpat .. '[^%.].*'
else
fullpat = fullpat .. '.*'
end
end
fullpat = fullpat .. '$'
debug_find("mkmatch_glob: [["..pat.."]] -> [["..fullpat.."]]")
return function(text)
local result = text:match(fullpat)
return result
end
end
local function mkmatch_null(pat, allow_wild_end)
local escaped_pat = '^' .. M.luapat_escape(pat)
if allow_wild_end then
escaped_pat = escaped_pat .. '.*'
end
escaped_pat = escaped_pat .. '$'
return function(text)
local result = text:match(escaped_pat)
return result
end
end
function do_matching_files(text, mk_matcher, escape)
debug_find("do_matching_files(text=[["..debug_ts(text).."]], mk_matcher, [["..debug_ts(escape).."]])")
-- First check for ~/
if text:sub(1,2) == "~/" then
text = os.getenv('HOME') .. '/' .. text:sub(3)
end
-- Split the pattern into parts separated by /
local is_abs
local patparts = {} -- the pieces of the pattern
if text then
if text:sub(1,1) == '/' then
text = text:sub(2)
is_abs = true
end
for part in text:gmatch('[^/]+') do
table.insert(patparts, part)
end
-- If tab on trailing /, then will want to complete on files in the
-- directory.
if text:sub(-1) == '/' then
table.insert(patparts, '')
end
end
debug_find("do_matching_files: patparts="..debug_ts(patparts))
-- partmatches[n] is a list of matches for patparts[n] at that level
local parts = { }
-- Set of directories to look in
local dirs = { }
-- The start depends on whether the path is absolute or relative
if is_abs then
table.insert(dirs, '/')
else
table.insert(dirs, './')
end
debug_find("do_matching_files: parts="..debug_ts(parts))
debug_find("do_matching_files: dirs="..debug_ts(dirs))
-- For each path section
for level, patpart in ipairs(patparts) do
debug_find("for each path: level="..debug_ts(level)..", patpart="..debug_ts(patpart))
local last = (level == #patparts)
debug_find("for each: last="..debug_ts(last))
-- If the last part, then allow trailing parts
-- TODO: if we complete from a middle-part, then
-- this test should be for where the cursor is.
local allow_wild_end = last
debug_find("for each: allow_wild_end="..debug_ts(allow_wild_end))
-- The set of paths for the following loop
local newdirs = {}
local matcher = mk_matcher(patpart, allow_wild_end)
-- For each possible directory at this level
for _,dir in ipairs(dirs) do
debug_find(" for dir [["..debug_ts(dir).."]]")
for fname in lfs.dir(dir) do
debug_find(" for fname [["..debug_ts(fname).."]]")
if matcher(fname) then
local fullpath
if dir == "./" then
fullpath = fname
else
fullpath = dir .. fname
end
debug_find(" for fname: fullpath=[["..debug_ts(fullpath).."]]")
local isdir = lfs.attributes(fullpath, 'mode') == 'directory'
debug_find(" for fname: isdir=[["..debug_ts(isdir).."]]")
-- Record this path if it's not a non-directory with more path
-- parts to go.
if isdir and not last then
table.insert(newdirs, fullpath .. '/')
elseif last then
table.insert(newdirs, fullpath)
end
end
end
end
-- Switch to the next level of items
dirs = newdirs
end -- loop through pattern parts
debug_find("do_matching_files: dirs="..debug_ts(dirs))
-- Find out the set of components at each level
-- parts[level] is a table { fname="f",fname2="f",dirname="d",fileordir="b", fname,fname2}
local parts = {}
for _,res in ipairs(dirs) do
local level = 1
debug_find(" res=[["..res.."]]")
local res_is_dir = lfs.attributes(res, 'mode') == 'directory'
-- Remove the leading / for this search
if is_abs then
assert(res:sub(1,1) == "/")
res = res:sub(2)
end
for piece in res:gmatch('[^/]*') do
local last = (level == #patparts)
local isdir = (not last) or res_is_dir
debug_find(" level="..level..", last="..tostring(last)..", isdir="..debug_ts(isdir))
debug_find(" piece=[["..debug_ts(piece).."]]")
ps = parts[level] or {}
parts[level] = ps
local type = isdir and "d" or "f"
if ps[piece] == nil then
ps[piece] = type
table.insert(ps, piece)
elseif ps[piece] ~= type then
ps[piece] = "b"
end
level = level + 1
end
end
debug_find("do_matching_files: #3: parts="..debug_ts(parts))
-- Now rebuild the pattern, with some ambiguities removed
local narrowed = false -- whether we've added more unambiguous info
local newparts = {}
-- keep absolute or relative
if is_abs then
table.insert(newparts, '/')
end
debug_find("Narrowing loop. patparts="..debug_ts(patparts))
for level,matches in ipairs(parts) do
debug_find("for level="..debug_ts(level)..", matches="..debug_ts(matches))
local last = (level == #parts)
debug_find(" last="..tostring(last)..", #matches="..tostring(#matches))
if #matches == 1 then
-- Only one thing, so use that.
local newpart = escape(matches[1])
debug_find(" newpart=[["..debug_ts(newpart).."]]")
if newpart ~= patparts[level] then
narrowed = true
end
debug_find(" narrowed=[["..debug_ts(narrowed).."]]")
table.insert(newparts, newpart)
if last and matches[matches[1]] == "d" then
table.insert(newparts, '/')
end
else
table.insert(newparts, patparts[level])
end
if not last then table.insert(newparts, '/') end
end
debug_find("After loop, newparts="..debug_ts(newparts))
local files
if narrowed then
files = { table.concat(newparts) }
else
debug_find("After loop not narrowed, but dirs="..debug_ts(dirs))
files = {}
table.sort(dirs)
for i,d in ipairs(dirs) do
files[i] = escape(d)
end
end
debug_find("do_matching_files: files="..debug_ts(files))
return files
end
-- Match filename exactly, with no escaping or wildcards etc.
function M.matching_files_nopat(text)
local escape = function(s) return s end
return do_matching_files(text, mkmatch_null, escape)
end
-- Find files with globs.
function M.matching_files(text, doescape)
-- Escape by default
local escape
if doescape == nil or doescape then
escape = M.glob_escape
else
escape = function(s) return s end
end
return do_matching_files(text, mkmatch_glob, escape)
end
-- Find files matching a Regex pattern (or a string match)
function find_matching_files_lua(pattern)
local results = {}
local pat = vi_regex.compile(pattern)
local function f(filename)
if (pat and pat:match(filename)) or filename:find(pattern, 1, true) then
results[#results+1] = filename
end
end
lfs.dir_foreach('.', f, { folders = { "build"}}, nil, false)
return results
end
function M.find_matching_files(pat_prefix, pat_suffix)
local findprg = vi_mode.state.variables.findprg
local pat = pat_prefix
if pat_suffix and #pat_suffix > 0 then
local wild = vi_mode.state.variables.findprg_wild or "*"
-- The ".*" depends on the findprg.
pat = pat .. wild .. pat_suffix
end
if findprg ~= nil then
local fproc = os.spawn(findprg .. " " .. pat)
local files = {}
while true do
local line, err, errmsg = fproc:read()
-- TODO: log errors and stderr
if line == nil then break end
table.insert(files, line)
end
fproc:close()
return files
else
-- Default to built-in function
return find_matching_files_lua(pat)
end
end
return M