Skip to content

Commit a6434d5

Browse files
authored
refactor(utils): remove custom glob to pattern logic (#1339)
Replace custom glob pattern conversion and scandir fallback with native vim.lpeg and vim.uv.fs_scandir for file matching. This simplifies code, removes redundant logic, and improves compatibility. Also remove related unit tests for glob_to_pattern. Closes #1331
1 parent 7b15d03 commit a6434d5

File tree

2 files changed

+64
-195
lines changed

2 files changed

+64
-195
lines changed

lua/CopilotChat/utils.lua

Lines changed: 64 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
local async = require('plenary.async')
22
local curl = require('plenary.curl')
3-
local scandir = require('plenary.scandir')
43
local log = require('plenary.log')
54

65
local M = {}
@@ -479,7 +478,7 @@ end
479478
---@class CopilotChat.utils.ScanOpts
480479
---@field max_count number? The maximum number of files to scan
481480
---@field max_depth number? The maximum depth to scan
482-
---@field glob? string The glob pattern to match files
481+
---@field pattern? string The glob pattern to match files
483482
---@field hidden? boolean Whether to include hidden files
484483
---@field no_ignore? boolean Whether to respect or ignore .gitignore
485484

@@ -527,19 +526,69 @@ M.glob = async.wrap(function(path, opts, callback)
527526
return
528527
end
529528

530-
-- Fall back to scandir if rg is not available or fails
531-
scandir.scan_dir_async(
532-
path,
533-
vim.tbl_deep_extend('force', opts, {
534-
depth = opts.max_depth,
535-
add_dirs = false,
536-
search_pattern = opts.glob and M.glob_to_pattern(opts.glob) or nil,
537-
respect_gitignore = not opts.no_ignore,
538-
on_exit = function(files)
539-
callback(filter_files(files, opts.max_count))
540-
end,
541-
})
542-
)
529+
-- Fallback to vim.uv.fs_scandir
530+
local matchers = {}
531+
if opts.pattern then
532+
local file_pattern = vim.glob.to_lpeg(opts.pattern)
533+
local path_pattern = vim.lpeg.P(path .. '/') * file_pattern
534+
535+
table.insert(matchers, function(name, dir)
536+
return file_pattern:match(name) or path_pattern:match(dir .. '/' .. name)
537+
end)
538+
end
539+
540+
if not opts.hidden then
541+
table.insert(matchers, function(name)
542+
return not name:match('^%.')
543+
end)
544+
end
545+
546+
local data = {}
547+
local next_dir = { path }
548+
local current_depths = { [path] = 1 }
549+
550+
local function read_dir(err, fd)
551+
local current_dir = table.remove(next_dir, 1)
552+
local depth = current_depths[current_dir] or 1
553+
554+
if not err and fd then
555+
while true do
556+
local name, typ = vim.uv.fs_scandir_next(fd)
557+
if name == nil then
558+
break
559+
end
560+
561+
local full_path = current_dir .. '/' .. name
562+
563+
if typ == 'directory' and not name:match('^%.git') then
564+
if not opts.max_depth or depth < opts.max_depth then
565+
table.insert(next_dir, full_path)
566+
current_depths[full_path] = depth + 1
567+
end
568+
else
569+
local match = true
570+
for _, matcher in ipairs(matchers) do
571+
if not matcher(name, current_dir) then
572+
match = false
573+
break
574+
end
575+
end
576+
577+
if match then
578+
table.insert(data, full_path)
579+
end
580+
end
581+
end
582+
end
583+
584+
if #next_dir == 0 then
585+
callback(data)
586+
else
587+
vim.uv.fs_scandir(next_dir[1], read_dir)
588+
end
589+
end
590+
591+
vim.uv.fs_scandir(path, read_dir)
543592
end, 3)
544593

545594
--- Grep a directory
@@ -783,136 +832,4 @@ function M.split_lines(text)
783832
return vim.split(text, '\r?\n', { trimempty = false })
784833
end
785834

786-
--- Convert glob pattern to regex pattern
787-
--- https://github.com/davidm/lua-glob-pattern/blob/master/lua/globtopattern.lua
788-
---@param g string The glob pattern
789-
---@return string
790-
function M.glob_to_pattern(g)
791-
local p = '^' -- pattern being built
792-
local i = 0 -- index in g
793-
local c -- char at index i in g.
794-
795-
-- unescape glob char
796-
local function unescape()
797-
if c == '\\' then
798-
i = i + 1
799-
c = g:sub(i, i)
800-
if c == '' then
801-
p = '[^]'
802-
return false
803-
end
804-
end
805-
return true
806-
end
807-
808-
-- escape pattern char
809-
local function escape(c)
810-
return c:match('^%w$') and c or '%' .. c
811-
end
812-
813-
-- Convert tokens at end of charset.
814-
local function charset_end()
815-
while 1 do
816-
if c == '' then
817-
p = '[^]'
818-
return false
819-
elseif c == ']' then
820-
p = p .. ']'
821-
break
822-
else
823-
if not unescape() then
824-
break
825-
end
826-
local c1 = c
827-
i = i + 1
828-
c = g:sub(i, i)
829-
if c == '' then
830-
p = '[^]'
831-
return false
832-
elseif c == '-' then
833-
i = i + 1
834-
c = g:sub(i, i)
835-
if c == '' then
836-
p = '[^]'
837-
return false
838-
elseif c == ']' then
839-
p = p .. escape(c1) .. '%-]'
840-
break
841-
else
842-
if not unescape() then
843-
break
844-
end
845-
p = p .. escape(c1) .. '-' .. escape(c)
846-
end
847-
elseif c == ']' then
848-
p = p .. escape(c1) .. ']'
849-
break
850-
else
851-
p = p .. escape(c1)
852-
i = i - 1 -- put back
853-
end
854-
end
855-
i = i + 1
856-
c = g:sub(i, i)
857-
end
858-
return true
859-
end
860-
861-
-- Convert tokens in charset.
862-
local function charset()
863-
i = i + 1
864-
c = g:sub(i, i)
865-
if c == '' or c == ']' then
866-
p = '[^]'
867-
return false
868-
elseif c == '^' or c == '!' then
869-
i = i + 1
870-
c = g:sub(i, i)
871-
if c == ']' then
872-
-- ignored
873-
else
874-
p = p .. '[^'
875-
if not charset_end() then
876-
return false
877-
end
878-
end
879-
else
880-
p = p .. '['
881-
if not charset_end() then
882-
return false
883-
end
884-
end
885-
return true
886-
end
887-
888-
-- Convert tokens.
889-
while 1 do
890-
i = i + 1
891-
c = g:sub(i, i)
892-
if c == '' then
893-
p = p .. '$'
894-
break
895-
elseif c == '?' then
896-
p = p .. '.'
897-
elseif c == '*' then
898-
p = p .. '.*'
899-
elseif c == '[' then
900-
if not charset() then
901-
break
902-
end
903-
elseif c == '\\' then
904-
i = i + 1
905-
c = g:sub(i, i)
906-
if c == '' then
907-
p = p .. '\\$'
908-
break
909-
end
910-
p = p .. escape(c)
911-
else
912-
p = p .. escape(c)
913-
end
914-
end
915-
return p
916-
end
917-
918835
return M

tests/utils_spec.lua

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,6 @@
11
local utils = require('CopilotChat.utils')
22

33
describe('CopilotChat.utils', function()
4-
local cases = {
5-
{ glob = '', expected = '^$' },
6-
{ glob = 'abc', expected = '^abc$' },
7-
{ glob = 'ab#/.', expected = '^ab%#%/%.$' },
8-
{ glob = '\\\\\\ab\\c\\', expected = '^%\\abc\\$' },
9-
10-
{ glob = 'abc.*', expected = '^abc%..*$', matches = { 'abc.txt', 'abc.' }, not_matches = { 'abc' } },
11-
{ glob = '??.txt', expected = '^..%.txt$' },
12-
13-
{ glob = 'a[]', expected = '[^]' },
14-
{ glob = 'a[^]b', expected = '^ab$' },
15-
{ glob = 'a[!]b', expected = '^ab$' },
16-
{ glob = 'a[a][b]z', expected = '^a[a][b]z$' },
17-
{ glob = 'a[a-f]z', expected = '^a[a-f]z$' },
18-
{ glob = 'a[a-f0-9]z', expected = '^a[a-f0-9]z$' },
19-
{ glob = 'a[a-f0-]z', expected = '^a[a-f0%-]z$' },
20-
{ glob = 'a[!a-f]z', expected = '^a[^a-f]z$' },
21-
{ glob = 'a[^a-f]z', expected = '^a[^a-f]z$' },
22-
{ glob = 'a[\\!\\^\\-z\\]]z', expected = '^a[%!%^%-z%]]z$' },
23-
{ glob = 'a[\\a-\\f]z', expected = '^a[a-f]z$' },
24-
25-
{ glob = 'a[', expected = '[^]' },
26-
{ glob = 'a[a-', expected = '[^]' },
27-
{ glob = 'a[a-b', expected = '[^]' },
28-
{ glob = 'a[!', expected = '[^]' },
29-
{ glob = 'a[!a', expected = '[^]' },
30-
{ glob = 'a[!a-', expected = '[^]' },
31-
{ glob = 'a[!a-b', expected = '[^]' },
32-
{ glob = 'a[!a-b\\]', expected = '[^]' },
33-
}
34-
35-
for _, case in ipairs(cases) do
36-
it('glob_to_pattern: ' .. case.glob, function()
37-
local pattern = utils.glob_to_pattern(case.glob)
38-
assert.equals(case.expected, pattern)
39-
if case.matches then
40-
for _, str in ipairs(case.matches) do
41-
assert.is_true(str:match(pattern) ~= nil)
42-
end
43-
end
44-
if case.not_matches then
45-
for _, str in ipairs(case.not_matches) do
46-
assert.is_false(str:match(pattern) ~= nil)
47-
end
48-
end
49-
end)
50-
end
51-
524
it('empty', function()
535
assert.is_true(utils.empty(nil))
546
assert.is_true(utils.empty(''))

0 commit comments

Comments
 (0)