Path aliases
VSCode's Path Autocomplete has a feature called path-autocomplete.pathMappings which allows you to define aliases for paths:
"path-autocomplete.pathMappings": {
"/test": "${folder}/src/Actions/test", // alias for /test
"/": "${folder}/src", // the absolute root folder is now /src,
"$root": ${folder}/src // the relative root folder is now /src
// or multiple folders for one mapping
"$root": ["${folder}/p1/src", "${folder}/p2/src"] // the root is now relative to both p1/src and p2/src
}
For example, you can define your root path (/) to point to your public directory and other useful things.
It would be nice if we can have a get_aliases function to support this.
Seconding this idea cause with vue projects in particular it's really convenient. :heart:
Here's my modified code to achieve the @ -> src effect.
local cmp = require 'cmp'
local NAME_REGEX = '\\%([^/\\\\:\\*?<>\'"`\\|]\\)'
-- edit one
local PATH_REGEX = vim.regex(([[\%(\%(/PAT*[^(@/|/)\\\\:\\*?<>\'"`\\| .~]\)\|\%(/\.\.\)\)*/\zePAT*$]]):gsub('PAT', NAME_REGEX))
local source = {}
local constants = {
max_lines = 20,
}
---@class cmp_path.Option
---@field public trailing_slash boolean
---@field public label_trailing_slash boolean
---@field public get_cwd fun(): string
---@type cmp_path.Option
local defaults = {
trailing_slash = false,
label_trailing_slash = true,
get_cwd = function(params)
return vim.fn.expand(('#%d:p:h'):format(params.context.bufnr))
end,
}
source.new = function()
return setmetatable({}, { __index = source })
end
source.get_trigger_characters = function()
return { '/', '.' }
end
source.get_keyword_pattern = function(self, params)
return NAME_REGEX .. '*'
end
source.complete = function(self, params, callback)
local option = self:_validate_option(params)
local dirname = self:_dirname(params, option)
if not dirname then
return callback()
end
-- print(dirname)
local include_hidden = string.sub(params.context.cursor_before_line, params.offset, params.offset) == '.'
self:_candidates(dirname, include_hidden, option, function(err, candidates)
if err then
return callback()
end
callback(candidates)
end)
end
source.resolve = function(self, completion_item, callback)
local data = completion_item.data
if data.stat and data.stat.type == 'file' then
local ok, documentation = pcall(function()
return self:_get_documentation(data.path, constants.max_lines)
end)
if ok then
completion_item.documentation = documentation
end
end
callback(completion_item)
end
source._dirname = function(self, params, option)
local current_directory = vim.fn.getcwd()
-- edit two
local replaced_string = string.gsub(params.context.cursor_before_line, '"(@)', '"' .. current_directory .. '/src')
local s = PATH_REGEX:match_str(replaced_string)
if not s then
return nil
end
-- edit two
local dirname = string.gsub(string.sub(replaced_string, s + 2), '%a*$', '') -- exclude '/'
local prefix = string.sub(replaced_string, 1, s + 1) -- include '/'
local buf_dirname = option.get_cwd(params)
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._candidates = function(_, dirname, include_hidden, option, callback)
local fs, err = vim.loop.fs_scandir(dirname)
if err then
return callback(err, nil)
end
local items = {}
local function create_item(name, fs_type)
if not (include_hidden or string.sub(name, 1, 1) ~= '.') then
return
end
local path = dirname .. '/' .. name
local stat = vim.loop.fs_stat(path)
local lstat = nil
if stat then
fs_type = stat.type
elseif fs_type == 'link' then
-- Broken symlink
lstat = vim.loop.fs_lstat(dirname)
if not lstat then
return
end
else
return
end
local item = {
label = name,
filterText = name,
insertText = name,
kind = cmp.lsp.CompletionItemKind.File,
data = {
path = path,
type = fs_type,
stat = stat,
lstat = lstat,
},
}
if fs_type == 'directory' then
item.kind = cmp.lsp.CompletionItemKind.Folder
if option.label_trailing_slash then
item.label = name .. '/'
else
item.label = name
end
item.insertText = name .. '/'
if not option.trailing_slash then
item.word = name
end
end
table.insert(items, item)
end
while true do
local name, fs_type, e = vim.loop.fs_scandir_next(fs)
if e then
return callback(fs_type, nil)
end
if not name then
break
end
create_item(name, fs_type)
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
---@return cmp_path.Option
source._validate_option = function(_, params)
local option = vim.tbl_deep_extend('keep', params.option, defaults)
vim.validate({
trailing_slash = { option.trailing_slash, 'boolean' },
label_trailing_slash = { option.label_trailing_slash, 'boolean' },
get_cwd = { option.get_cwd, 'function' },
})
return option
end
source._get_documentation = function(_, filename, count)
local binary = assert(io.open(filename, 'rb'))
local first_kb = binary:read(1024)
if first_kb:find('\0') then
return { kind = cmp.lsp.MarkupKind.PlainText, value = 'binary file' }
end
local contents = {}
for content in first_kb:gmatch("[^\r\n]+") do
table.insert(contents, content)
if count ~= nil and #contents >= count then
break
end
end
local filetype = vim.filetype.match({ filename = filename })
if not filetype then
return { kind = cmp.lsp.MarkupKind.PlainText, value = table.concat(contents, '\n') }
end
table.insert(contents, 1, '```' .. filetype)
table.insert(contents, '```')
return { kind = cmp.lsp.MarkupKind.Markdown, value = table.concat(contents, '\n') }
end
return source
@kola-web
Would really appreciate it if you can submit that as a PR!
@musjj have created a pr I've forked one myself, so if you're in a hurry, you can use my forked version first. link
@kola-web ~~Why does the solution look so complicated?~~ Nevermind, I just realized that you only changed some parts of the original code of this repo. Thanks for sharing.
@musjj @kola-web @saysora
First of all, let's talk/check about path-autocompletion mentioned by OP.
By path-autocompletion itself, it's probably useless without compiler/bundler support. That is, you can tweak your editor settings to have something like this without an error(which doesn't mean it's correct):
import Foo from '@cool_prefix/foo' // it "work" from the point of view of your editor.
// since it can understand those jargons after your tweak.
But those environments that run your code do NOT have your path-autocompletion settings. So you still need a compiler/bundler, e.g. tsc or webpack resolve.alias in my case, to do the dirty work: compiling the above code into the expanded path:
import Foo from '/expanded/real/path/foo'
With this background knowledge in mind, I just wrote a working version (so yes, you still need a compiler/transpiler, as explained ^) to fulfill the requirement of OP. It has some no-harm caveats(linked to my comments of code) Or maybe I should say "TODOs". In short, It reads the tsconfig.json located at your project root(i.e. the cwd you were on when you started neovim).
By the way, it's probably the is currently the only way I know to auto-complete .css files when an alias is used in your import path. (Otherwise, I don't bother to modify the code of this plugin, as debugging regex is a bit painful) This default behavior of hrsh7th/cmp-path is reasonable since it's not designed to be language-dependent. But the trade-off of this is that it didn't read tsconfig.json before my extension. The provider of auto-completion, i.e. tsserver/typescript-tools in my case, could be (but I'm not sure) responsible for showing those .css files. I have opened some feature requests here and there, e.g.: https://github.com/pmizio/typescript-tools.nvim/issues/242. But before I got any reply, I think you guys could try my work and maybe find some bugs.
So given this tsconfig.json: (unrelated fields removed)
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"/*": [ "./public/*" ],
"@/*": [ "./src/reactApp/*" ],
"@components/*": [ "./src/reactApp/components/*" ],
"@another/*": [ "./src/firebase/" ]
},
},
}
You can have what OP described in the title:
By the way, if you need to use this kind of amplification in vue, you need to add "allowJs": true
@kola-web Thanks for your concern. it's indeed required for not just vue, but also webpack in my case. That's because tsconfig.json by default excludes all js/jsx files for type analysis and intellisense. I omitted it in my explanation above to make the code concise.