You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
307 lines
7.9 KiB
Lua
307 lines
7.9 KiB
Lua
local utils = require "fzf-lua.utils"
|
|
local string_sub = string.sub
|
|
local string_byte = string.byte
|
|
|
|
local M = {}
|
|
|
|
M.separator = function()
|
|
return '/'
|
|
end
|
|
|
|
M.dot_byte = string_byte('.')
|
|
M.separator_byte = string_byte(M.separator())
|
|
|
|
M.starts_with_separator = function(path)
|
|
return string_byte(path, 1) == M.separator_byte
|
|
-- return path:find("^"..M.separator()) == 1
|
|
end
|
|
|
|
M.starts_with_cwd = function(path)
|
|
return #path>1
|
|
and string_byte(path, 1) == M.dot_byte
|
|
and string_byte(path, 2) == M.separator_byte
|
|
-- return path:match("^."..M.separator()) ~= nil
|
|
end
|
|
|
|
M.strip_cwd_prefix = function(path)
|
|
return #path>2 and path:sub(3)
|
|
end
|
|
|
|
function M.tail(path)
|
|
local os_sep = string_byte(M.separator())
|
|
|
|
for i=#path,1,-1 do
|
|
if string_byte(path, i) == os_sep then
|
|
return path:sub(i+1)
|
|
end
|
|
end
|
|
return path
|
|
end
|
|
|
|
function M.extension(path)
|
|
for i=#path,1,-1 do
|
|
if string_byte(path, i) == 46 then
|
|
return path:sub(i+1)
|
|
end
|
|
end
|
|
return path
|
|
end
|
|
|
|
function M.to_matching_str(path)
|
|
-- return path:gsub('(%-)', '(%%-)'):gsub('(%.)', '(%%.)'):gsub('(%_)', '(%%_)')
|
|
-- above is missing other lua special chars like '+' etc (#315)
|
|
return utils.lua_regex_escape(path)
|
|
end
|
|
|
|
function M.join(paths)
|
|
-- gsub to remove double separator
|
|
return table.concat(paths, M.separator()):gsub(
|
|
M.separator()..M.separator(), M.separator())
|
|
end
|
|
|
|
function M.split(path)
|
|
return path:gmatch('[^'..M.separator()..']+'..M.separator()..'?')
|
|
end
|
|
|
|
---Get the basename of the given path.
|
|
---@param path string
|
|
---@return string
|
|
function M.basename(path)
|
|
path = M.remove_trailing(path)
|
|
local i = path:match("^.*()" .. M.separator())
|
|
if not i then return path end
|
|
return path:sub(i + 1, #path)
|
|
end
|
|
|
|
---Get the path to the parent directory of the given path. Returns `nil` if the
|
|
---path has no parent.
|
|
---@param path string
|
|
---@param remove_trailing boolean
|
|
---@return string|nil
|
|
function M.parent(path, remove_trailing)
|
|
path = " " .. M.remove_trailing(path)
|
|
local i = path:match("^.+()" .. M.separator())
|
|
if not i then return nil end
|
|
path = path:sub(2, i)
|
|
if remove_trailing then
|
|
path = M.remove_trailing(path)
|
|
end
|
|
return path
|
|
end
|
|
|
|
---Get a path relative to another path.
|
|
---@param path string
|
|
---@param relative_to string
|
|
---@return string
|
|
function M.relative(path, relative_to)
|
|
local p, _ = path:gsub("^" .. M.to_matching_str(M.add_trailing(relative_to)), "")
|
|
return p
|
|
end
|
|
|
|
function M.is_relative(path, relative_to)
|
|
local p = path:match("^" .. M.to_matching_str(M.add_trailing(relative_to)))
|
|
return p ~= nil
|
|
end
|
|
|
|
function M.add_trailing(path)
|
|
if path:sub(-1) == M.separator() then
|
|
return path
|
|
end
|
|
|
|
return path..M.separator()
|
|
end
|
|
|
|
function M.remove_trailing(path)
|
|
local p, _ = path:gsub(M.separator()..'$', '')
|
|
return p
|
|
end
|
|
|
|
local function find_next(str, char, start_idx)
|
|
local i_char = string_byte(char, 1)
|
|
for i=start_idx or 1,#str do
|
|
if string_byte(str, i) == i_char then
|
|
return i
|
|
end
|
|
end
|
|
end
|
|
|
|
function M.shorten(path, max_len)
|
|
local sep = M.separator()
|
|
local parts = {}
|
|
local start_idx = 1
|
|
max_len = max_len or 1
|
|
repeat
|
|
local i = find_next(path, sep, start_idx)
|
|
local end_idx = i and start_idx+math.min(i-start_idx, max_len)-1 or nil
|
|
table.insert(parts, string_sub(path, start_idx, end_idx))
|
|
if i then start_idx = i+1 end
|
|
until not i
|
|
return table.concat(parts, sep)
|
|
end
|
|
|
|
function M.lengthen(path)
|
|
return vim.fn.glob(path:gsub(M.separator(), "%*"..M.separator())
|
|
-- remove the starting '*/' if any
|
|
:gsub("^%*"..M.separator(), M.separator()))
|
|
end
|
|
|
|
local function lastIndexOf(haystack, needle)
|
|
local i=haystack:match(".*"..needle.."()")
|
|
if i==nil then return nil else return i-1 end
|
|
end
|
|
|
|
local function stripBeforeLastOccurrenceOf(str, sep)
|
|
local idx = lastIndexOf(str, sep) or 0
|
|
return str:sub(idx+1), idx
|
|
end
|
|
|
|
|
|
function M.entry_to_ctag(entry, noesc)
|
|
local ctag = entry:match("%:.-/^?\t?(.*)/")
|
|
-- if tag name contains a slash we could
|
|
-- have the wrong match, most tags start
|
|
-- with ^ so try to match based on that
|
|
ctag = ctag and ctag:match("/^(.*)") or ctag
|
|
if ctag and not noesc then
|
|
-- required escapes for vim.fn.search()
|
|
-- \ ] ~ *
|
|
ctag = ctag:gsub('[\\%]~*]',
|
|
function(x)
|
|
return '\\' .. x
|
|
end)
|
|
end
|
|
return ctag
|
|
end
|
|
|
|
function M.entry_to_location(entry, opts)
|
|
local uri, line, col = entry:match("^(.*://.*):(%d+):(%d+):")
|
|
line = line and tonumber(line) or 1
|
|
col = col and tonumber(col) or 1
|
|
if opts and opts.path_shorten then
|
|
uri = uri:match("^.*://") .. M.lengthen(uri:match("^.*://(.*)"))
|
|
end
|
|
return {
|
|
stripped = entry,
|
|
line = line,
|
|
col = col,
|
|
uri = uri,
|
|
range = {
|
|
start = {
|
|
line = line-1,
|
|
character = col-1,
|
|
}
|
|
}
|
|
}
|
|
end
|
|
|
|
function M.entry_to_file(entry, opts)
|
|
opts = opts or {}
|
|
local cwd = opts.cwd
|
|
local force_uri = opts.force_uri
|
|
-- Remove ansi coloring and prefixed icons
|
|
entry = utils.strip_ansi_coloring(entry)
|
|
local stripped, idx = stripBeforeLastOccurrenceOf(entry, utils.nbsp)
|
|
local isURI = stripped:match("^%a+://")
|
|
-- Prepend cwd before constructing the URI (#341)
|
|
if cwd and #cwd>0 and not isURI and
|
|
not M.starts_with_separator(stripped) then
|
|
stripped = M.join({cwd, stripped})
|
|
end
|
|
-- #336: force LSP jumps using 'vim.lsp.util.jump_to_location'
|
|
-- so that LSP entries are added to the tag stack
|
|
if not isURI and force_uri then
|
|
isURI = true
|
|
stripped = "file://" .. stripped
|
|
end
|
|
-- entries from 'buffers' contain '[<bufnr>]'
|
|
-- buffer placeholder always comes before the nbsp
|
|
local bufnr = idx>1 and entry:sub(1, idx):match("%[(%d+)") or nil
|
|
if isURI and not bufnr then
|
|
-- Issue #195, when using nvim-jdtls
|
|
-- https://github.com/mfussenegger/nvim-jdtls
|
|
-- LSP entries inside .jar files appear as URIs
|
|
-- 'jdt://' which can then be opened with
|
|
-- 'vim.lsp.util.jump_to_location' or
|
|
-- 'lua require('jdtls').open_jdt_link(vim.fn.expand('jdt://...'))'
|
|
-- Convert to location item so we can use 'jump_to_location'
|
|
-- This can also work with any 'file://' prefixes
|
|
return M.entry_to_location(stripped, opts)
|
|
end
|
|
local s = utils.strsplit(stripped, ":")
|
|
if not s[1] then return {} end
|
|
local file = s[1]
|
|
local line = tonumber(s[2])
|
|
local col = tonumber(s[3])
|
|
local terminal
|
|
if bufnr then
|
|
terminal = utils.is_term_buffer(bufnr)
|
|
if terminal then
|
|
file, line = stripped:match("([^:]+):(%d+)")
|
|
end
|
|
end
|
|
if opts.path_shorten then
|
|
file = M.lengthen(file)
|
|
end
|
|
return {
|
|
stripped = stripped,
|
|
bufnr = tonumber(bufnr),
|
|
bufname = bufnr and vim.api.nvim_buf_is_valid(tonumber(bufnr))
|
|
and vim.api.nvim_buf_get_name(tonumber(bufnr)),
|
|
terminal = terminal,
|
|
path = file,
|
|
line = tonumber(line) or 1,
|
|
col = tonumber(col) or 1,
|
|
}
|
|
end
|
|
|
|
function M.git_cwd(cmd, opts)
|
|
-- backward compat, used to be single cwd param
|
|
local o = opts or {}
|
|
if type(o) == 'string' then
|
|
o = { cwd = o }
|
|
end
|
|
local git_args = {
|
|
{ "cwd", "-C" },
|
|
{ "git_dir", "--git-dir" },
|
|
{ "git_worktree", "--work-tree" },
|
|
}
|
|
if type(cmd) == 'string' then
|
|
local args = ""
|
|
for _, a in ipairs(git_args) do
|
|
if o[a[1]] then
|
|
o[a[1]] = vim.fn.expand(o[a[1]])
|
|
args = args .. ("%s %s "):format(a[2], vim.fn.shellescape(o[a[1]]))
|
|
end
|
|
end
|
|
cmd = cmd:gsub("^git ", "git " .. args)
|
|
else
|
|
local idx = 2
|
|
cmd = utils.tbl_deep_clone(cmd)
|
|
for _, a in ipairs(git_args) do
|
|
if o[a[1]] then
|
|
o[a[1]] = vim.fn.expand(o[a[1]])
|
|
table.insert(cmd, idx, a[2])
|
|
table.insert(cmd, idx+1, o[a[1]])
|
|
idx = idx + 2
|
|
end
|
|
end
|
|
end
|
|
return cmd
|
|
end
|
|
|
|
function M.is_git_repo(opts, noerr)
|
|
return not not M.git_root(opts, noerr)
|
|
end
|
|
|
|
function M.git_root(opts, noerr)
|
|
local cmd = M.git_cwd({"git", "rev-parse", "--show-toplevel"}, opts)
|
|
local output, err = utils.io_systemlist(cmd)
|
|
if err ~= 0 then
|
|
if not noerr then utils.info(unpack(output)) end
|
|
return nil
|
|
end
|
|
return output[1]
|
|
end
|
|
|
|
return M
|