navigator.lua/lua/navigator/lspwrapper.lua

529 lines
15 KiB
Lua

local M = {}
local util = require "navigator.util"
local nvim_0_6 = util.nvim_0_6()
local gutil = require "guihua.util"
local lsp = require "vim.lsp"
local api = vim.api
local log = require"navigator.util".log
local lerr = require"navigator.util".error
local trace = require"navigator.util".trace
local symbol_kind = require"navigator.lspclient.lspkind".symbol_kind
local cwd = vim.loop.cwd()
local is_win = vim.loop.os_uname().sysname:find("Windows")
local path_sep = require"navigator.util".path_sep()
local path_cur = require"navigator.util".path_cur()
cwd = gutil.add_pec(cwd)
local ts_nodes = require('navigator.lru').new(1000, 1024 * 1024)
local ts_nodes_time = require('navigator.lru').new(1000)
local TS_analysis_enabled = require"navigator".config_values().treesitter_analysis
-- extract symbol from range
function M.get_symbol(text, range)
if range == nil then
return ""
end
return string.sub(text, range.start.character + 1, range['end'].character)
end
local function check_lhs(text, symbol)
local find = require'guihua.util'.word_find
local s = find(text, symbol)
local eq = string.find(text, '=') or 0
local eq2 = string.find(text, '==') or 0
local eq3 = string.find(text, '!=') or 0
local eq4 = string.find(text, '~=') or 0
if not s or not eq then
return false
end
if s < eq and eq ~= eq2 then
trace(symbol, "modified")
end
if eq == eq3 + 1 then
return false
end
if eq == eq4 + 1 then
return false
end
return s < eq and eq ~= eq2
end
function M.lines_from_locations(locations, include_filename)
local fnamemodify = (function(filename)
if include_filename then
return vim.fn.fnamemodify(filename, ":~:.") .. ":"
else
return ""
end
end)
local lines = {}
for _, loc in ipairs(locations) do
table.insert(lines,
(fnamemodify(loc["filename"]) .. loc["lnum"] .. ":" .. loc["col"] .. ": " .. vim.trim(loc["text"])))
end
return lines
end
function M.symbols_to_items(result)
local locations = {}
-- log(result)
for i = 1, #result do
local item = result[i].location
if item ~= nil and item.range ~= nil then
item.kind = result[i].kind
local kind = symbol_kind(item.kind)
item.name = result[i].name -- symbol name
item.text = result[i].name
if kind ~= nil then
item.text = kind .. ": " .. item.text
end
item.filename = vim.uri_to_fname(item.uri)
item.display_filename = item.filename:gsub(cwd .. path_sep, path_cur, 1)
if item.range == nil or item.range.start == nil then
log("range not set", result[i], item)
end
item.lnum = item.range.start.line + 1
if item.containerName ~= nil then
item.text = "" .. item.containerName .. item.text
end
table.insert(locations, item)
end
end
-- local items = locations_to_items(locations)
-- log(locations[1])
return locations
end
local function extract_result(results_lsp)
if results_lsp then
local results = {}
for _, server_results in pairs(results_lsp) do
if server_results.result then
vim.list_extend(results, server_results.result)
end
end
return results
end
end
function M.check_capabilities(feature, client_id)
local clients = lsp.buf_get_clients(client_id or 0)
local supported_client = false
for _, client in pairs(clients) do
supported_client = client.resolved_capabilities[feature]
if supported_client then
break
end
end
if supported_client then
return true
else
if #clients == 0 then
log("LSP: no client attached")
else
trace("LSP: server does not support " .. feature)
end
return false
end
end
function M.call_sync(method, params, opts, handler)
params = params or {}
opts = opts or {}
local results_lsp, err = lsp.buf_request_sync(0, method, params, opts.timeout or vim.g.navtator_timeout or 1000)
if nvim_0_6() then
handler(err, extract_result(results_lsp), {method = method}, nil)
else
handler(err, method, extract_result(results_lsp), nil, nil)
end
end
function M.call_async(method, params, handler)
params = params or {}
local callback = function(...)
util.show(...)
handler(...)
end
return lsp.buf_request(0, method, params, callback)
-- results_lsp, canceller
end
local function ts_functions(uri)
local unload_bufnr
local ts_enabled, _ = pcall(require, "nvim-treesitter.locals")
if not ts_enabled or not TS_analysis_enabled then
lerr("ts not enabled")
return nil
end
local ts_func = require"navigator.treesitter".buf_func
local bufnr = vim.uri_to_bufnr(uri)
local x = os.clock()
trace(ts_nodes)
local tsnodes = ts_nodes:get(uri)
if tsnodes ~= nil then
trace("get data from cache")
local t = ts_nodes_time:get(uri) or 0
local fname = vim.uri_to_fname(uri)
local modified = vim.fn.getftime(fname)
if modified <= t then
trace(t, modified)
return tsnodes
else
ts_nodes:delete(uri)
ts_nodes_time:delete(uri)
end
end
local unload = false
if not api.nvim_buf_is_loaded(bufnr) then
trace("! load buf !", uri, bufnr)
vim.fn.bufload(bufnr)
-- vim.api.nvim_buf_detach(bufnr) -- if user opens the buffer later, it prevents user attach event
unload = true
end
local funcs = ts_func(bufnr)
if unload then
unload_bufnr = bufnr
end
ts_nodes:set(uri, funcs)
ts_nodes_time:set(uri, os.time())
trace(funcs, ts_nodes:get(uri))
trace(string.format("elapsed time: %.4f\n", os.clock() - x)) -- how long it tooks
return funcs, unload_bufnr
end
local function ts_definition(uri, range)
local unload_bufnr
local ts_enabled, _ = pcall(require, "nvim-treesitter.locals")
if not ts_enabled or not TS_analysis_enabled then
lerr("ts not enabled")
return nil
end
local key = string.format('%s_%d_%d_%d', uri, range.start.line, range.start.character, range['end'].line)
local tsnode = ts_nodes:get(key)
local ftime = ts_nodes_time:get(key)
local fname = vim.uri_to_fname(uri)
local modified = vim.fn.getftime(fname)
if tsnodes and modified <= ftime then
log('ts def from cache')
return tsnode
end
local ts_def = require"navigator.treesitter".find_definition
local bufnr = vim.uri_to_bufnr(uri)
local x = os.clock()
trace(ts_nodes)
local unload = false
if not api.nvim_buf_is_loaded(bufnr) then
log("! load buf !", uri, bufnr)
vim.fn.bufload(bufnr)
unload = true
end
local def_range = ts_def(range, bufnr) or {}
if unload then
unload_bufnr = bufnr
end
trace(string.format(" ts def elapsed time: %.4f\n", os.clock() - x), def_range) -- how long it takes
ts_nodes:set(key, def_range)
ts_nodes_time:set(key, x)
return def_range, unload_bufnr
end
local function find_ts_func_by_range(funcs, range)
if funcs == nil or range == nil then
return nil
end
local result = {}
trace(funcs, range)
for _, value in pairs(funcs) do
local func_range = value.node_scope
-- note treesitter is C style
if func_range and func_range.start.line <= range.start.line and func_range['end'].line >= range['end'].line then
table.insert(result, value)
end
end
return result
end
local function order_locations(locations)
table.sort(locations, function(i, j)
if i.uri == j.uri then
if i.range and i.range.start then
return i.range.start.line < j.range.start.line
end
return false
else
return i.uri < j.uri
end
end)
return locations
end
local function slice_locations(locations, max_items)
local cut = -1
if #locations > max_items then
local uri = locations[max_items]
for i = max_items + 1, #locations do
if uri ~= locations[i] and not brk then
cut = i
break
end
end
end
local first_part, second_part = locations, {}
if cut > 1 and cut < #locations then
first_part = vim.list_slice(locations, 1, cut)
second_part = vim.list_slice(locations, cut + 1, #locations)
end
return first_part, second_part
end
local function test_locations()
local locations = {
{uri = '1', range = {start = {line = 1}}}, {uri = '2', range = {start = {line = 2}}},
{uri = '2', range = {start = {line = 3}}}, {uri = '1', range = {start = {line = 3}}},
{uri = '1', range = {start = {line = 4}}}, {uri = '3', range = {start = {line = 4}}},
{uri = '3', range = {start = {line = 4}}}
}
local second_part
order_locations(locations)
local locations, second_part = slice_locations(locations, 3)
log(locations, second_part)
end
function M.locations_to_items(locations, max_items)
max_items = max_items or 100000 --
if not locations or vim.tbl_isempty(locations) then
print("list not avalible")
return
end
local width = 4
local items = {} -- lsp.util.locations_to_items(locations)
-- items and locations may not matching
local uri_def = {}
order_locations(locations)
local second_part
locations, second_part = slice_locations(locations, max_items)
trace(locations)
local cut = -1
local unload_bufnrs = {}
for i, loc in ipairs(locations) do
local funcs = nil
local item = lsp.util.locations_to_items({loc})[1]
-- log(item)
item.range = locations[i].range or locations[i].targetRange
item.uri = locations[i].uri or locations[i].targetUri
if is_win then
log(item.uri) -- file:///C:/path/to/file
log(cwd)
end
-- only load top 30 file.
local proj_file = item.uri:find(cwd) or is_win or i < 30
local unload, def
if TS_analysis_enabled and proj_file then
funcs, unload = ts_functions(item.uri)
if unload then
table.insert(unload_bufnrs, unload)
end
if not uri_def[item.uri] then
-- find def in file
def, unload = ts_definition(item.uri, item.range)
if def and def.start then
uri_def[item.uri] = def
if def.start then -- find for the 1st time
for j = 1, #items do
if items[j].uri == item.uri and items[j].range.start.line == def.start.line then
items[j].definition = true
end
end
end
else
if uri_def[item.uri] == false then
uri_def[item.uri] = {} -- no def in file, TODO: it is tricky the definition is in another file and it is the
-- only occurrence
else
uri_def[item.uri] = false -- no def in file
end
end
if unload then
table.insert(unload_bufnrs, unload)
end
end
trace(uri_def[item.uri], item.range) -- set to log if need to get all in rnge
local def = uri_def[item.uri]
if def and def.start and item.range then
if def.start.line == item.range.start.line then
log("ts def in current line")
item.definition = true
end
end
end
item.filename = assert(vim.uri_to_fname(item.uri))
local filename = item.filename:gsub(cwd .. path_sep, path_cur, 1)
item.display_filename = filename or item.filename
item.call_by = find_ts_func_by_range(funcs, item.range)
item.rpath = util.get_relative_path(cwd, item.filename)
width = math.max(width, #item.text)
item.symbol_name = M.get_symbol(item.text, item.range)
item.lhs = check_lhs(item.text, item.symbol_name)
table.insert(items, item)
end
trace(uri_def)
-- defer release new open buffer
if #unload_bufnrs > 10 then -- load too many?
vim.defer_fn(function()
for i, bufnr_unload in ipairs(unload_bufnrs) do
if api.nvim_buf_is_loaded(bufnr_unload) and i > 10 then
api.nvim_buf_delete(bufnr_unload, {unload = true})
end
end
end, 100)
end
return items, width + 24, second_part -- TODO handle long line?
end
function M.apply_action(action_chosen)
assert(action_chosen ~= nil, "action must not be nil")
local bufnr = api.nvim_get_current_buf()
local switch = string.format("silent b %d", bufnr)
if action_chosen.edit or type(action_chosen.command) == "table" then
if action_chosen.edit then
vim.lsp.util.apply_workspace_edit(action_chosen.edit)
end
if type(action_chosen.command) == "table" then
-- switch buff
vim.cmd(switch)
vim.lsp.buf.execute_command(action_chosen.command)
end
else
vim.cmd(switch)
vim.lsp.buf.execute_command(action_chosen)
end
log(action_chosen)
end
local function apply_action(action, client, ctx)
log(action, client)
if action.edit then
require('vim.lsp.util').apply_workspace_edit(action.edit)
end
if action.command then
local command = type(action.command) == 'table' and action.command or action
local fn = vim.lsp.commands[command.command]
if fn then
local enriched_ctx = vim.deepcopy(ctx)
enriched_ctx.client_id = client.id
fn(command, ctx)
else
require('vim.lsp.buf').execute_command(command)
end
end
end
function M.on_user_choice(action_tuple, ctx)
if not action_tuple then
return
end
log(action_tuple)
-- textDocument/codeAction can return either Command[] or CodeAction[]
--
-- CodeAction
-- ...
-- edit?: WorkspaceEdit -- <- must be applied before command
-- command?: Command
--
-- Command:
-- title: string
-- command: string
-- arguments?: any[]
--
local client = vim.lsp.get_client_by_id(action_tuple[1])
local action = action_tuple[2]
if not action.edit and client and type(client.resolved_capabilities.code_action) == 'table'
and client.resolved_capabilities.code_action.resolveProvider then
client.request('codeAction/resolve', action, function(err, resolved_action)
if err then
vim.notify(err.code .. ': ' .. err.message, vim.log.levels.ERROR)
return
end
apply_action(resolved_action, client, ctx)
end)
else
apply_action(action, client, ctx)
end
end
function M.symbol_to_items(locations)
if not locations or vim.tbl_isempty(locations) then
print("list not avalible")
return
end
local items = {} -- lsp.util.locations_to_items(locations)
-- items and locations may not matching
table.sort(locations, function(i, j)
if i.uri == j.uri then
if i.range and i.range.start then
return i.range.start.line < j.range.start.line
end
return false
else
return i.uri < j.uri
end
end)
for i, _ in ipairs(locations) do
local item = {} -- lsp.util.locations_to_items({loc})[1]
item.uri = locations[i].uri
item.range = locations[i].range
item.filename = assert(vim.uri_to_fname(item.uri))
local filename = item.filename:gsub(cwd .. path_sep, path_cur, 1)
item.display_filename = filename or item.filename
item.rpath = util.get_relative_path(cwd, item.filename)
table.insert(items, item)
end
return items
end
function M.request(method, hdlr) -- e.g textDocument/reference
local bufnr = vim.api.nvim_get_current_buf()
local ref_params = vim.lsp.util.make_position_params()
vim.lsp.for_each_buffer_client(bufnr, function(client, client_id, _bufnr)
client.request(method, ref_params, hdlr, bufnr)
end)
end
return M