Skip to content

Commit 5787816

Browse files
committed
feat: rework path source
1 parent f5d4dae commit 5787816

File tree

4 files changed

+188
-8
lines changed

4 files changed

+188
-8
lines changed

lua/blink/cmp/sources/path.lua

-8
This file was deleted.

lua/blink/cmp/sources/path/fs.lua

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
local async = require('blink.cmp.sources.lib.async')
2+
local uv = vim.uv
3+
local fs = {}
4+
5+
--- Scans a directory asynchronously in a loop until
6+
--- it finds all entries
7+
--- @param path string
8+
--- @return blink.cmp.Task
9+
function fs.scan_dir_async(path)
10+
local max_entries = 200
11+
return async.task.new(function(resolve, reject)
12+
uv.fs_opendir(path, function(err, handle)
13+
if err ~= nil or handle == nil then return reject(err) end
14+
15+
local all_entries = {}
16+
17+
local function read_dir()
18+
uv.fs_readdir(handle, function(err, entries)
19+
if err ~= nil or entries == nil then return reject(err) end
20+
21+
vim.list_extend(all_entries, entries)
22+
if #entries == max_entries then
23+
read_dir()
24+
else
25+
resolve(all_entries)
26+
end
27+
end)
28+
end
29+
read_dir()
30+
end, max_entries)
31+
end)
32+
end
33+
34+
--- @param entries { name: string, type: string }[]
35+
--- @return blink.cmp.Task
36+
function fs.fs_stat_all(cwd, entries)
37+
local tasks = {}
38+
for _, entry in ipairs(entries) do
39+
table.insert(
40+
tasks,
41+
async.task.new(function(resolve, reject)
42+
uv.fs_stat(cwd .. '/' .. entry.name, function(err, stat)
43+
if err then return reject(err) end
44+
resolve({ name = entry.name, type = entry.type, stat = stat })
45+
end)
46+
end)
47+
)
48+
end
49+
return async.task.await_all(tasks):map(function(tasks_results)
50+
local resolved_entries = {}
51+
for _, entry in ipairs(tasks_results) do
52+
if entry.status == async.STATUS.COMPLETED then table.insert(resolved_entries, entry.result) end
53+
end
54+
return resolved_entries
55+
end)
56+
end
57+
58+
return fs

lua/blink/cmp/sources/path/init.lua

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
-- credit to https://github.com/hrsh7th/cmp-path for the original implementation
2+
-- and https://codeberg.org/FelipeLema/cmp-async-path for the async implementation
3+
4+
local path = {}
5+
local NAME_REGEX = '\\%([^/\\\\:\\*?<>\'"`\\|]\\)'
6+
local PATH_REGEX =
7+
assert(vim.regex(([[\%(\%(/PAT*[^/\\\\:\\*?<>\'"`\\| .~]\)\|\%(/\.\.\)\)*/\zePAT*$]]):gsub('PAT', NAME_REGEX)))
8+
9+
function path.new(opts)
10+
local self = setmetatable({}, { __index = path })
11+
12+
opts = vim.tbl_deep_extend('keep', opts, {
13+
trailing_slash = false,
14+
label_trailing_slash = true,
15+
get_cwd = function(context) return vim.fn.expand(('#%d:p:h'):format(context.bufnr)) end,
16+
show_hidden_files_by_default = false,
17+
})
18+
vim.validate({
19+
trailing_slash = { opts.trailing_slash, 'boolean' },
20+
label_trailing_slash = { opts.label_trailing_slash, 'boolean' },
21+
get_cwd = { opts.get_cwd, 'function' },
22+
show_hidden_files_by_default = { opts.show_hidden_files_by_default, 'boolean' },
23+
})
24+
25+
self.opts = opts or {}
26+
return self
27+
end
28+
29+
function path:get_trigger_characters() return { '/', '.' } end
30+
31+
function path:get_completions(context, callback)
32+
local lib = require('blink.cmp.sources.path.lib')
33+
34+
local dirname = lib.dirname(PATH_REGEX, self.opts.get_cwd, context)
35+
if not dirname then return callback() end
36+
37+
local include_hidden = self.opts.show_hidden_files_by_default
38+
or string.sub(context.line, context.bounds.start_col - 1, context.bounds.start_col - 1) == '.'
39+
lib
40+
.candidates(dirname, include_hidden, self.opts)
41+
:map(
42+
function(candidates)
43+
callback({ is_incomplete_forward = false, is_incomplete_backward = true, items = candidates })
44+
end
45+
)
46+
:catch(function() callback() end)
47+
end
48+
49+
return path

lua/blink/cmp/sources/path/lib.lua

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
local lib = {}
2+
3+
--- @param path_regex vim.regex
4+
--- @param get_cwd fun(context: blink.cmp.CompletionContext): string
5+
--- @param context blink.cmp.CompletionContext
6+
function lib.dirname(path_regex, get_cwd, context)
7+
local line_before_cursor = context.line:sub(1, context.cursor[2])
8+
local s = path_regex:match_str(line_before_cursor)
9+
if not s then return nil end
10+
11+
local dirname = string.gsub(string.sub(line_before_cursor, s + 2), '%a*$', '') -- exclude '/'
12+
local prefix = string.sub(line_before_cursor, 1, s + 1) -- include '/'
13+
14+
local buf_dirname = get_cwd(context)
15+
if vim.api.nvim_get_mode().mode == 'c' then buf_dirname = vim.fn.getcwd() end
16+
if prefix:match('%.%./$') then return vim.fn.resolve(buf_dirname .. '/../' .. dirname) end
17+
if prefix:match('%./$') or prefix:match('"$') or prefix:match("'$") then
18+
return vim.fn.resolve(buf_dirname .. '/' .. dirname)
19+
end
20+
if prefix:match('~/$') then return vim.fn.resolve(vim.fn.expand('~') .. '/' .. dirname) end
21+
local env_var_name = prefix:match('%$([%a_]+)/$')
22+
if env_var_name then
23+
local env_var_value = vim.fn.getenv(env_var_name)
24+
if env_var_value ~= vim.NIL then return vim.fn.resolve(env_var_value .. '/' .. dirname) end
25+
end
26+
if prefix:match('/$') then
27+
local accept = true
28+
-- Ignore URL components
29+
accept = accept and not prefix:match('%a/$')
30+
-- Ignore URL scheme
31+
accept = accept and not prefix:match('%a+:/$') and not prefix:match('%a+://$')
32+
-- Ignore HTML closing tags
33+
accept = accept and not prefix:match('</$')
34+
-- Ignore math calculation
35+
accept = accept and not prefix:match('[%d%)]%s*/$')
36+
-- Ignore / comment
37+
accept = accept and (not prefix:match('^[%s/]*$') or not self:_is_slash_comment())
38+
if accept then return vim.fn.resolve('/' .. dirname) end
39+
end
40+
return nil
41+
end
42+
43+
--- @param dirname string
44+
--- @param include_hidden boolean
45+
--- @param opts table
46+
function lib.candidates(dirname, include_hidden, opts)
47+
local fs = require('blink.cmp.sources.path.fs')
48+
return fs.scan_dir_async(dirname)
49+
:map(function(entries) return fs.fs_stat_all(dirname, entries) end)
50+
:map(function(entries)
51+
return vim.tbl_filter(function(entry) return include_hidden or entry.name ~= '.' end, entries)
52+
end)
53+
:map(function(entries)
54+
return vim.tbl_map(function(entry) return lib.entry_to_completion_item(entry, opts) end, entries)
55+
end)
56+
end
57+
58+
function lib.is_slash_comment(_)
59+
local commentstring = vim.bo.commentstring or ''
60+
local no_filetype = vim.bo.filetype == ''
61+
local is_slash_comment = false
62+
is_slash_comment = is_slash_comment or commentstring:match('/%*')
63+
is_slash_comment = is_slash_comment or commentstring:match('//')
64+
return is_slash_comment and not no_filetype
65+
end
66+
67+
--- @param entry { name: string, type: string, stat: table }
68+
--- @param opts table
69+
--- @return blink.cmp.CompletionItem[]
70+
function lib.entry_to_completion_item(entry, opts)
71+
local is_dir = entry.type == 'directory'
72+
return {
73+
label = (opts.label_trailing_slash and is_dir) and entry.name .. '/' or entry.name,
74+
kind = is_dir and vim.lsp.protocol.CompletionItemKind.Folder or vim.lsp.protocol.CompletionItemKind.File,
75+
insertText = is_dir and entry.name .. '/' or entry.name,
76+
word = opts.trailing_slash and entry.name or nil,
77+
data = { path = entry.name, type = entry.type, stat = entry.stat },
78+
}
79+
end
80+
81+
return lib

0 commit comments

Comments
 (0)