cmp-async-path/lua/cmp_path/init.lua
hrsh7th e30d3fdee6
Merge pull request #16 from sudoforge/10/relative-paths-without-a-leading-slash
feat: support relative paths without a leading forward slash
2021-11-29 12:22:52 +09:00

217 lines
5.7 KiB
Lua

local cmp = require'cmp'
local NAME_REGEX = '\\%([^/\\\\:\\*?<>\'"`\\|]\\)'
local PATH_REGEX = vim.regex(([[\%([/"\']PAT\+\)*[/"\']\zePAT*$]]):gsub('PAT', NAME_REGEX))
local source = {}
local defaults = {
max_lines = 20,
}
source.new = function()
return setmetatable({}, { __index = source })
end
source.get_trigger_characters = function()
return { '/', '.', '"', '\'' }
end
source.get_keyword_pattern = function()
return NAME_REGEX .. '*'
end
source.complete = function(self, params, callback)
local dirname = self:_dirname(params)
if not dirname then
return callback()
end
local stat = self:_stat(dirname)
if not stat then
return callback()
end
self:_candidates(params, dirname, params.offset, function(err, candidates)
if err then
return callback()
end
callback(candidates)
end)
end
source._dirname = function(self, params)
local s = PATH_REGEX:match_str(params.context.cursor_before_line)
if not s then
return nil
end
local dirname = string.gsub(string.sub(params.context.cursor_before_line, s + 2), '%a*$', '') -- exclude '/'
local prefix = string.sub(params.context.cursor_before_line, 1, s + 1) -- include '/'
local buf_dirname = vim.fn.expand(('#%d:p:h'):format(params.context.bufnr))
if vim.api.nvim_get_mode().mode == 'c' then
buf_dirname = vim.fn.getcwd()
end
if prefix:match('%.%./$') then
return vim.fn.resolve(buf_dirname .. '/../' .. dirname)
end
if (prefix:match('%./$') or prefix:match('"$') or prefix:match('\'$')) then
return vim.fn.resolve(buf_dirname .. '/' .. dirname)
end
if prefix:match('~/$') then
return vim.fn.resolve(vim.fn.expand('~') .. '/' .. dirname)
end
local env_var_name = prefix:match('%$([%a_]+)/$')
if env_var_name then
local env_var_value = vim.fn.getenv(env_var_name)
if env_var_value ~= vim.NIL then
return vim.fn.resolve(env_var_value .. '/' .. dirname)
end
end
if prefix:match('/$') then
local accept = true
-- Ignore URL components
accept = accept and not prefix:match('%a/$')
-- Ignore URL scheme
accept = accept and not prefix:match('%a+:/$') and not prefix:match('%a+://$')
-- Ignore HTML closing tags
accept = accept and not prefix:match('</$')
-- Ignore math calculation
accept = accept and not prefix:match('[%d%)]%s*/$')
-- Ignore / comment
accept = accept and (not prefix:match('^[%s/]*$') or not self:_is_slash_comment())
if accept then
return vim.fn.resolve('/' .. dirname)
end
end
return nil
end
source._stat = function(_, path)
local stat = vim.loop.fs_stat(path)
if stat then
return stat
end
return nil
end
local function lines_from(file, count)
local bfile = assert(io.open(file, 'rb'))
local first_k = bfile:read(1024)
if first_k:find('\0') then
return {'binary file'}
end
local lines = {'```'}
for line in first_k:gmatch("[^\r\n]+") do
lines[#lines + 1] = line
if count ~= nil and #lines >= count then
break
end
end
lines[#lines + 1] = '```'
return lines
end
source._candidates = function(_, params, dirname, offset, callback)
local fs, err = vim.loop.fs_scandir(dirname)
if err then
return callback(err, nil)
end
local items = {}
local include_hidden = string.sub(params.context.cursor_before_line, offset, offset) == '.'
while true do
local name, type, e = vim.loop.fs_scandir_next(fs)
if e then
return callback(type, nil)
end
if not name then
break
end
local accept = false
accept = accept or include_hidden
accept = accept or name:sub(1, 1) ~= '.'
local stat = nil
-- Stat when fs_scandir_next doesn't return file type
if type == nil then
stat = vim.loop.fs_stat(dirname .. '/' .. name)
if not stat then
break
end
type = stat.type
end
-- Create items
if accept then
if type == 'directory' then
table.insert(items, {
word = name,
label = name,
insertText = name .. '/',
kind = cmp.lsp.CompletionItemKind.Folder,
})
elseif type == 'link' then
if not stat then
stat = vim.loop.fs_stat(dirname .. '/' .. name)
end
if stat then
if stat.type == 'directory' then
table.insert(items, {
word = name,
label = name,
insertText = name .. '/',
kind = cmp.lsp.CompletionItemKind.Folder,
})
else
table.insert(items, {
label = name,
filterText = name,
insertText = name,
kind = cmp.lsp.CompletionItemKind.File,
data = {path = dirname .. '/' .. name},
})
end
end
elseif type == 'file' then
table.insert(items, {
label = name,
filterText = name,
insertText = name,
kind = cmp.lsp.CompletionItemKind.File,
data = {path = dirname .. '/' .. name},
})
end
end
end
callback(nil, items)
end
source._is_slash_comment = function(_)
local commentstring = vim.bo.commentstring or ''
local no_filetype = vim.bo.filetype == ''
local is_slash_comment = false
is_slash_comment = is_slash_comment or commentstring:match('/%*')
is_slash_comment = is_slash_comment or commentstring:match('//')
return is_slash_comment and not no_filetype
end
function source:resolve(completion_item, callback)
if completion_item.kind == cmp.lsp.CompletionItemKind.File then
local path = completion_item.data.path
if not vim.startswith(path, '/dev') then
local ok, preview_lines = pcall(lines_from, path, defaults.max_lines)
if ok then
completion_item.documentation = preview_lines
end
end
end
callback(completion_item)
end
return source