275 lines
7.4 KiB
Lua
275 lines
7.4 KiB
Lua
local sk = vim.lsp.protocol.SymbolKind
|
|
local symbol_kinds = {
|
|
[sk.Class] = true,
|
|
[sk.Method] = true,
|
|
[sk.Constructor] = true,
|
|
[sk.Enum] = true,
|
|
[sk.Interface] = true,
|
|
[sk.Function] = true,
|
|
[sk.Struct] = true,
|
|
}
|
|
|
|
local function range_contains(range, line0, col0)
|
|
if not range then
|
|
return false
|
|
end
|
|
|
|
local start_line = range.start.line
|
|
local start_col = range.start.character
|
|
local end_line = range['end'].line
|
|
local end_col = range['end'].character
|
|
|
|
if line0 < start_line or line0 > end_line then
|
|
return false
|
|
end
|
|
if line0 == start_line and col0 < start_col then
|
|
return false
|
|
end
|
|
if line0 == end_line and col0 > end_col then
|
|
return false
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
local function range_size(range)
|
|
-- Weight lines heavily so a single-line range is always smaller than a multi-line one
|
|
return (range['end'].line - range.start.line) * 100000 + (range['end'].character - range.start.character)
|
|
end
|
|
|
|
local function append_document_symbols(symbols, out, parent_name)
|
|
for _, symbol in ipairs(symbols or {}) do
|
|
local symbol_name = symbol.name
|
|
local full_name = parent_name and (parent_name .. '.' .. symbol_name) or symbol_name
|
|
table.insert(out, {
|
|
name = full_name,
|
|
kind = symbol.kind,
|
|
range = symbol.range,
|
|
})
|
|
append_document_symbols(symbol.children, out, full_name)
|
|
end
|
|
end
|
|
|
|
local function append_symbol_information(symbols, out)
|
|
for _, symbol in ipairs(symbols or {}) do
|
|
local name = symbol.name
|
|
if symbol.containerName and symbol.containerName ~= '' then
|
|
name = symbol.containerName .. '.' .. name
|
|
end
|
|
table.insert(out, {
|
|
name = name,
|
|
kind = symbol.kind,
|
|
range = symbol.location and symbol.location.range,
|
|
})
|
|
end
|
|
end
|
|
|
|
local function get_symbol_at_cursor()
|
|
if #vim.lsp.get_clients { bufnr = 0 } == 0 then
|
|
return nil
|
|
end
|
|
|
|
local params = { textDocument = vim.lsp.util.make_text_document_params() }
|
|
local responses = vim.lsp.buf_request_sync(0, 'textDocument/documentSymbol', params, 500)
|
|
if not responses then
|
|
return nil
|
|
end
|
|
|
|
local cursor = vim.api.nvim_win_get_cursor(0)
|
|
local line0 = cursor[1] - 1
|
|
-- LSP ranges use UTF-16 offsets; convert from byte offset for correct matching
|
|
-- on files with non-ASCII characters (e.g. CJK, emoji).
|
|
local line_text = vim.api.nvim_buf_get_lines(0, line0, line0 + 1, false)[1] or ''
|
|
local col0 = vim.str_utfindex(line_text, 'utf-16', cursor[2])
|
|
local symbols = {}
|
|
|
|
for _, response in pairs(responses) do
|
|
if not response.error then
|
|
local result = response.result
|
|
if type(result) == 'table' and result[1] then
|
|
if result[1].selectionRange or result[1].children then
|
|
append_document_symbols(result, symbols, nil)
|
|
else
|
|
append_symbol_information(result, symbols)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local best_symbol, best_size
|
|
for _, symbol in ipairs(symbols) do
|
|
if symbol_kinds[symbol.kind] and range_contains(symbol.range, line0, col0) then
|
|
local size = range_size(symbol.range)
|
|
if not best_size or size < best_size then
|
|
best_size = size
|
|
best_symbol = symbol.name
|
|
end
|
|
end
|
|
end
|
|
|
|
return best_symbol
|
|
end
|
|
|
|
local function get_relative_path_from_root(file_path)
|
|
local root = vim.fs.root(file_path, { '.git' }) or vim.fn.getcwd()
|
|
|
|
if vim.fs.relpath then
|
|
local rel = vim.fs.relpath(root, file_path)
|
|
if rel then
|
|
return rel
|
|
end
|
|
end
|
|
|
|
local prefix = root
|
|
if not prefix:match '/$' then
|
|
prefix = prefix .. '/'
|
|
end
|
|
if file_path:sub(1, #prefix) == prefix then
|
|
return file_path:sub(#prefix + 1)
|
|
end
|
|
|
|
return file_path
|
|
end
|
|
|
|
local function get_file_parts()
|
|
local file_path = vim.api.nvim_buf_get_name(0)
|
|
if file_path == '' then
|
|
vim.notify('No file path for current buffer', vim.log.levels.WARN)
|
|
return nil
|
|
end
|
|
|
|
return {
|
|
file_path = file_path,
|
|
file_rel = get_relative_path_from_root(file_path),
|
|
}
|
|
end
|
|
|
|
local function get_location_parts()
|
|
local parts = get_file_parts()
|
|
if not parts then
|
|
return nil
|
|
end
|
|
|
|
local mode = vim.api.nvim_get_mode().mode
|
|
local loc
|
|
|
|
if mode == 'v' or mode == 'V' or mode == '\22' then
|
|
-- Use 'v' (visual anchor) and '.' (cursor) for the live selection.
|
|
-- '</'> marks are only reliable after exiting visual mode (e.g. ':' mappings),
|
|
-- but <Cmd> mappings stay in visual mode so those marks are stale.
|
|
local start_pos = vim.fn.getpos 'v'
|
|
local end_pos = vim.fn.getpos '.'
|
|
local s_line, s_col = start_pos[2], start_pos[3]
|
|
local e_line, e_col = end_pos[2], end_pos[3]
|
|
|
|
-- getpos('v') returns zeros when called outside a real visual context; fall back.
|
|
if s_line == 0 then
|
|
local cur = vim.api.nvim_win_get_cursor(0)
|
|
s_line, s_col = cur[1], cur[2] + 1
|
|
e_line, e_col = s_line, s_col
|
|
elseif s_line > e_line or (s_line == e_line and s_col > e_col) then
|
|
s_line, e_line = e_line, s_line
|
|
s_col, e_col = e_col, s_col
|
|
end
|
|
|
|
loc = string.format('%d:%d-%d:%d', s_line, s_col, e_line, e_col)
|
|
else
|
|
local cursor = vim.api.nvim_win_get_cursor(0)
|
|
local line_num = cursor[1]
|
|
local line_text = vim.api.nvim_buf_get_lines(0, line_num - 1, line_num, false)[1] or ''
|
|
local end_col = math.max(vim.fn.strchars(line_text), 1)
|
|
loc = string.format('%d:%d-%d:%d', line_num, 1, line_num, end_col)
|
|
end
|
|
|
|
return {
|
|
file_path = parts.file_path,
|
|
file_rel = parts.file_rel,
|
|
loc = loc,
|
|
symbol = get_symbol_at_cursor(),
|
|
}
|
|
end
|
|
|
|
local function yank(label, value)
|
|
vim.fn.setreg('"', value)
|
|
pcall(vim.fn.setreg, '+', value)
|
|
vim.notify('Copied ' .. label .. ': ' .. value, vim.log.levels.INFO)
|
|
end
|
|
|
|
local function copy_absolute_location_reference()
|
|
local parts = get_location_parts()
|
|
if not parts then
|
|
return
|
|
end
|
|
|
|
local location = string.format('%s:%s', parts.file_path, parts.loc)
|
|
if parts.symbol and parts.symbol ~= '' then
|
|
location = location .. ' symbol:' .. parts.symbol
|
|
end
|
|
|
|
yank('absolute location', location)
|
|
end
|
|
|
|
local function copy_relative_location_reference()
|
|
local parts = get_location_parts()
|
|
if not parts then
|
|
return
|
|
end
|
|
|
|
local symbol = parts.symbol and parts.symbol ~= '' and parts.symbol or 'none'
|
|
yank('relative location', string.format('@%s loc:%s symbol:%s', parts.file_rel, parts.loc, symbol))
|
|
end
|
|
|
|
local function copy_buffer_file_reference()
|
|
local parts = get_file_parts()
|
|
if not parts then
|
|
return
|
|
end
|
|
|
|
yank('buffer file', string.format('@%s', parts.file_rel))
|
|
end
|
|
|
|
local function copy_buffer_directory_reference()
|
|
local parts = get_file_parts()
|
|
if not parts then
|
|
return
|
|
end
|
|
|
|
local abs_dir = vim.fn.fnamemodify(parts.file_path, ':h')
|
|
local rel_dir = get_relative_path_from_root(abs_dir)
|
|
yank('buffer directory', string.format('@%s', rel_dir))
|
|
end
|
|
|
|
return {
|
|
'folke/which-key.nvim',
|
|
keys = {
|
|
{
|
|
'<leader>ya',
|
|
copy_absolute_location_reference,
|
|
mode = { 'n', 'x' },
|
|
desc = '[Y]ank [A]bsolute ref',
|
|
},
|
|
{
|
|
'<leader>yr',
|
|
copy_relative_location_reference,
|
|
mode = { 'n', 'x' },
|
|
desc = '[Y]ank [R]elative ref',
|
|
},
|
|
{
|
|
'<leader>yf',
|
|
copy_buffer_file_reference,
|
|
mode = { 'n', 'x' },
|
|
desc = '[Y]ank Buffer [F]ile',
|
|
},
|
|
{
|
|
'<leader>yd',
|
|
copy_buffer_directory_reference,
|
|
mode = { 'n', 'x' },
|
|
desc = '[Y]ank Buffer [D]irectory',
|
|
},
|
|
},
|
|
opts = function(_, opts)
|
|
opts.spec = opts.spec or {}
|
|
table.insert(opts.spec, { '<leader>y', group = '[Y]ank' })
|
|
end,
|
|
}
|