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
|
local regex = require('blink.cmp.sources.path.regex')
local lib = {}
--- @param get_cwd fun(context: blink.cmp.Context): string
--- @param context blink.cmp.Context
function lib.dirname(get_cwd, context)
-- HACK: move this :sub logic into the context?
-- it's not obvious that you need to avoid going back a char if the start_col == end_col
local line_before_cursor = context.line:sub(1, context.bounds.start_col - (context.bounds.length == 0 and 1 or 0))
local s = regex.PATH:match_str(line_before_cursor)
if not s then return nil end
local dirname = string.gsub(string.sub(line_before_cursor, s + 2), regex.NAME .. '*$', '') -- exclude '/'
local prefix = string.sub(line_before_cursor, 1, s + 1) -- include '/'
local buf_dirname = get_cwd(context)
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 lib.is_slash_comment())
if accept then return vim.fn.resolve('/' .. dirname) end
end
-- Windows drive letter (C:/)
if prefix:match('(%a:)[/\\]$') then return vim.fn.resolve(prefix:match('(%a:)[/\\]$') .. '/' .. dirname) end
return nil
end
--- @param context blink.cmp.Context
--- @param dirname string
--- @param include_hidden boolean
--- @param opts table
function lib.candidates(context, dirname, include_hidden, opts)
local fs = require('blink.cmp.sources.path.fs')
local ranges = lib.get_text_edit_ranges(context)
return fs.scan_dir_async(dirname)
:map(function(entries) return fs.fs_stat_all(dirname, entries) end)
:map(function(entries)
return vim.tbl_filter(function(entry) return include_hidden or entry.name:sub(1, 1) ~= '.' end, entries)
end)
:map(function(entries)
return vim.tbl_map(
function(entry)
return lib.entry_to_completion_item(
entry,
dirname,
entry.type == 'directory' and ranges.directory or ranges.file,
opts
)
end,
entries
)
end)
end
function lib.is_slash_comment()
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
--- @param entry { name: string, type: string, stat: table }
--- @param dirname string
--- @param range lsp.Range
--- @param opts table
--- @return blink.cmp.CompletionItem[]
function lib.entry_to_completion_item(entry, dirname, range, opts)
local is_dir = entry.type == 'directory'
local CompletionItemKind = require('blink.cmp.types').CompletionItemKind
local insert_text = is_dir and opts.trailing_slash and entry.name .. '/' or entry.name
return {
label = (opts.label_trailing_slash and is_dir) and entry.name .. '/' or entry.name,
kind = is_dir and CompletionItemKind.Folder or CompletionItemKind.File,
insertText = insert_text,
textEdit = { newText = insert_text, range = range },
sortText = (is_dir and '1' or '2') .. entry.name:lower(), -- Sort directories before files
data = { path = entry.name, full_path = dirname .. '/' .. entry.name, type = entry.type, stat = entry.stat },
}
end
--- @param context blink.cmp.Context
--- @return { file: lsp.Range, directory: lsp.Range }
function lib.get_text_edit_ranges(context)
local line_before_cursor = context.line:sub(1, context.cursor[2])
local next_letter_is_slash = context.line:sub(context.cursor[2] + 1, context.cursor[2] + 1) == '/'
local parts = vim.split(line_before_cursor, '/')
local last_part = parts[#parts]
-- TODO: return the insert and replace ranges, instead of only the insert range
return {
file = {
start = { line = context.cursor[1] - 1, character = context.cursor[2] - #last_part },
['end'] = { line = context.cursor[1] - 1, character = context.cursor[2] },
},
directory = {
start = { line = context.cursor[1] - 1, character = context.cursor[2] - #last_part },
-- replace the slash after the cursor, if it exists
['end'] = { line = context.cursor[1] - 1, character = context.cursor[2] + (next_letter_is_slash and 1 or 0) },
},
}
end
return lib
|