From 9d5f2efdbd5e27f01fa44f6408386cffed06d0d2 Mon Sep 17 00:00:00 2001 From: ray-x Date: Mon, 19 Apr 2021 12:56:32 +1000 Subject: [PATCH] init commit --- README.md | 88 +++++ lua/.gitignore | 1 + lua/navigator.lua | 52 +++ lua/navigator/codeAction.lua | 209 ++++++++++++ lua/navigator/definition.lua | 101 ++++++ lua/navigator/diagnostics.lua | 83 +++++ lua/navigator/gui.lua | 149 +++++++++ lua/navigator/hierarchy.lua | 85 +++++ lua/navigator/hover.lua | 23 ++ lua/navigator/implementation.lua | 38 +++ lua/navigator/lspclient/attach.lua | 74 +++++ lua/navigator/lspclient/clients.lua | 219 +++++++++++++ lua/navigator/lspclient/config.lua | 29 ++ lua/navigator/lspclient/highlight.lua | 48 +++ lua/navigator/lspclient/init.lua | 7 + lua/navigator/lspclient/lspkind.lua | 38 +++ lua/navigator/lspclient/mapping.lua | 142 ++++++++ lua/navigator/lspwrapper.lua | 128 ++++++++ lua/navigator/protocal.json | 453 ++++++++++++++++++++++++++ lua/navigator/reference.lua | 32 ++ lua/navigator/symbols.lua | 115 +++++++ lua/navigator/util.lua | 231 +++++++++++++ plugin/navigator.vim | 24 ++ 23 files changed, 2369 insertions(+) create mode 100644 README.md create mode 100644 lua/.gitignore create mode 100644 lua/navigator.lua create mode 100644 lua/navigator/codeAction.lua create mode 100644 lua/navigator/definition.lua create mode 100644 lua/navigator/diagnostics.lua create mode 100644 lua/navigator/gui.lua create mode 100644 lua/navigator/hierarchy.lua create mode 100644 lua/navigator/hover.lua create mode 100644 lua/navigator/implementation.lua create mode 100644 lua/navigator/lspclient/attach.lua create mode 100644 lua/navigator/lspclient/clients.lua create mode 100644 lua/navigator/lspclient/config.lua create mode 100644 lua/navigator/lspclient/highlight.lua create mode 100644 lua/navigator/lspclient/init.lua create mode 100644 lua/navigator/lspclient/lspkind.lua create mode 100644 lua/navigator/lspclient/mapping.lua create mode 100644 lua/navigator/lspwrapper.lua create mode 100644 lua/navigator/protocal.json create mode 100644 lua/navigator/reference.lua create mode 100644 lua/navigator/symbols.lua create mode 100644 lua/navigator/util.lua create mode 100644 plugin/navigator.vim diff --git a/README.md b/README.md new file mode 100644 index 0000000..3f0617f --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ +# Navigator + +GUI for neovim lsp with a collection of most used LSP/treesitter functions. +Easy code navigation. Based on LSP. + +# Features: + +- LSP easy setup. Support some of the most commonly used lsp client setup +- GUI +- fzy search +- Better navigation for diagnostic errors, Navigate through files that contain errors/warnings +- Group references/implementation/incomming/outgoing based on file names. + +# Why a new plugin + +After installed a handful of lsp plugins, I still got ~500 loc for lsp and still increasing. Reason is that I need +to tune the plugins to fit my requirements. + +# Similar projects / special mentions: + +- [nvim-lsputils](https://github.com/RishabhRD/nvim-lsputils) +- [nvim-fzy](https://github.com/mfussenegger/nvim-fzy.git) +- [fuzzy](https://github.com/amirrezaask/fuzzy.nvim) +- [lspsaga](https://github.com/glepnir/lspsaga.nvim) + +# Install + +You can remove your lspconfig setup and use this plugin. +The plugin depends on [guihua.lua](https://github.com/ray-x/guihua.lua), which provides gui and fzy support. + +```vim +Plug 'ray-x/guihua.lua', {'do': 'cd lua/fzy && make' } +Plug 'ray-x/navigator.lua' +``` + +Packer + +```lua + +use {'ray-x/navigator.lua', requires = {'ray-x/guihua.lua', run = 'cd lua/fzy && make'}} + +``` + +## Setup + +```lua +lua require'navigator'.setup() +``` + +## Screenshots + +### Reference + +![reference](https://github.com/ray-x/files/blob/master/img/navigator/ref.gif?raw=true) + +### Diagnostic + +![diagnostic](https://github.com/ray-x/files/blob/master/img/navigator/diag.jpg?raw=true) + +### Implementation + +![implementation](https://github.com/ray-x/files/blob/master/img/navigator/implemention.jpg?raw=true) + +### Fzy search in reference + +![fzy_reference](https://github.com/ray-x/files/blob/master/img/navigator/fzy_reference.jpg?raw=true) + +### Code actions + +![code actions](https://github.com/ray-x/files/blob/master/img/navigator/codeaction.jpg?raw=true) + +### Code preview with highlight + +![code preview](https://github.com/ray-x/files/blob/master/img/navigator/preview_with_hl.jpg?raw=true) + +### Call hierarchy (incomming/outgoing) + +![incomming](https://github.com/ray-x/files/blob/master/img/navigator/incomming.jpg?raw=true) + +# Todo + +- Early phase, bugs expected +- Async (some of the requests is slow on large codebase and might be good to use co-rountine) +- More clients. I use go, python, js/ts, java, c/cpp, lua most of the time. Do not test other languages (e.g rust, swift etc) + +``` + +``` diff --git a/lua/.gitignore b/lua/.gitignore new file mode 100644 index 0000000..53c4b98 --- /dev/null +++ b/lua/.gitignore @@ -0,0 +1 @@ +TODO.txt diff --git a/lua/navigator.lua b/lua/navigator.lua new file mode 100644 index 0000000..99006a8 --- /dev/null +++ b/lua/navigator.lua @@ -0,0 +1,52 @@ +local M = {} + +M.config_values ={ + debug = false, -- log output + code_action_icon = ' ', + code_action_prompt = { + enable = true, + sign = true, + sign_priority = 40, + virtual_text = true, + }, +} + +vim.cmd("command! -nargs=0 LspLog call v:lua.open_lsp_log()") +vim.cmd("command! -nargs=0 LspRestart call v:lua.reload_lsp()") + +local extend_config = function(opts) + opts = opts or {} + if next(opts) == nil then return end + for key,value in pairs(opts) do + if M.config_values[key] == nil then + error(string.format('[] Key %s not valid',key)) + return + end + if type(M.config_values[key]) == 'table' then + for k,v in pairs(value) do + M.config_values[key][k] = v + end + else + M.config_values[key] = value + end + end +end + +M.setup = function(cfg) + extend_config(cfg) + -- print("loading navigator") + require('navigator.lspclient').setup(M.config_values) + require('navigator.reference') + require('navigator.definition') + require('navigator.hierarchy') + require('navigator.implementation') + + print("navigator loader") + + if M.config_values.code_action_prompt.enable then + vim.cmd [[autocmd CursorHold,CursorHoldI * lua require'navigator.codeAction'.code_action_prompt()]] + end + +end + +return M diff --git a/lua/navigator/codeAction.lua b/lua/navigator/codeAction.lua new file mode 100644 index 0000000..9b81220 --- /dev/null +++ b/lua/navigator/codeAction.lua @@ -0,0 +1,209 @@ +local util = require "navigator.util" +local log = util.log +local code_action = {} +local gui = require "navigator.gui" +local config = require("navigator").config_values +local api = vim.api +function code_action.code_action_handler(err, _, actions, num, _, _, customSelectionHandler) + -- log(actions) + if actions == nil or vim.tbl_isempty(actions) then + print("No code actions available") + return + end + local data = {" Auto Fix Apply Exit"} + for i, action in ipairs(actions) do + local title = action.title:gsub("\r\n", "\\r\\n") + title = title:gsub("\n", "\\n") + title = string.format("[%d] %s", i, title) + table.insert(data, title) + end + local width = 0 + for _, str in ipairs(data) do + if #str > width then + width = #str + end + end + -- log(data) + + local function apply_action(idx) + local action_chosen = actions[idx - 1] + 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 + vim.lsp.buf.execute_command(action_chosen.command) + end + else + vim.lsp.buf.execute_command(action_chosen) + end + end + + gui.new_list_view { + items = data, + width = width + 4, + loc = "top_center", + relative = "cursor", + rawdata = true, + data = data, + on_confirm = function(pos) + if pos < 2 then + pos = 2 + end + apply_action(pos) + end, + on_move = function(pos) + if pos < 2 then + pos = 2 + end + local l = data[pos] + -- log("on move", l) + end + } +end + +-- https://github.com/glepnir/lspsaga.nvim/blob/main/lua/lspsaga/codeaction.lua +-- lspsaga has a clever design to inject code action indicator +local sign_group = "nvcodeaction" +local get_namespace = function() + return api.nvim_create_namespace(sign_group) +end + +local get_current_winid = function() + return api.nvim_get_current_win() +end + +local sign_name = "NavigatorLightBulb" + +if vim.tbl_isempty(vim.fn.sign_getdefined(sign_name)) then + vim.fn.sign_define(sign_name, {text = config.code_action_icon, texthl = "LspDiagnosticsSignHint"}) +end + +local function _update_virtual_text(line) + local namespace = get_namespace() + api.nvim_buf_clear_namespace(0, namespace, 0, -1) + + if line then + local icon_with_indent = " " .. config.code_action_icon + + -- log("updat text", line, icon_with_indent) + api.nvim_buf_set_extmark( + 0, + namespace, + line, + -1, + { + virt_text = {{icon_with_indent, "LspDiagnosticsSignHint"}}, + virt_text_pos = "overlay", + hl_mode = "combine" + } + ) + end +end + +local function _update_sign(line) + local winid = get_current_winid() + if code_action[winid] and code_action[winid].lightbulb_line ~= 0 then + vim.fn.sign_unplace(sign_group, {id = code_action[winid].lightbulb_line, buffer = "%"}) + end + + if line then + --log("updatasign", line, sign_group, sign_name) + vim.fn.sign_place( + line, + sign_group, + sign_name, + "%", + {lnum = line + 1, priority = config.code_action_prompt.sign_priority} + ) + code_action[winid].lightbulb_line = line + end +end + +local need_check_diagnostic = { + ["go"] = true, + ["python"] = true +} + +function code_action:render_action_virtual_text(line, diagnostics) + return function(_, _, actions) + + if actions == nil or type(actions) ~= "table" or vim.tbl_isempty(actions) then + if config.code_action_prompt.virtual_text then + _update_virtual_text(nil) + end + if config.code_action_prompt.sign then + _update_sign(nil) + end + else + if config.code_action_prompt.sign then + if need_check_diagnostic[vim.bo.filetype] then + if next(diagnostics) == nil then + _update_sign(nil) + else + _update_sign(line) + end + else + _update_sign(line) + end + end + + if config.code_action_prompt.virtual_text then + if need_check_diagnostic[vim.bo.filetype] then + if next(diagnostics) == nil then + _update_virtual_text(nil) + else + _update_virtual_text(line) + end + else + _update_virtual_text(line) + end + end + end + end +end + +local special_buffers = { + ["LspSagaCodecode_action"] = true, + ["lspsagafinder"] = true, + ["NvimTree"] = true, + ["vist"] = true, + ["lspinfo"] = true, + ["markdown"] = true, + ["text"] = true +} +-- local action_call_back = function (_,_) +-- return Action:action_callback() +-- end + +local action_vritual_call_back = function (line,diagnostics) + return code_action:render_action_virtual_text(line,diagnostics) +end + +local code_action_req = function(_call_back_fn, diagnostics) + local context = {diagnostics = diagnostics} + local params = vim.lsp.util.make_range_params() + params.context = context + local line = params.range.start.line + local callback = _call_back_fn(line, diagnostics) + vim.lsp.buf_request(0, "textDocument/codeAction", params, callback) +end + +-- code_action.code_action = function() +-- local diagnostics = vim.lsp.diagnostic.get_line_diagnostics() +-- code_action_req(action_call_back, diagnostics) +-- end + +code_action.code_action_prompt = function() + if special_buffers[vim.bo.filetype] then + return + end + + local diagnostics = vim.lsp.diagnostic.get_line_diagnostics() + local winid = get_current_winid() + code_action[winid] = code_action[winid] or {} + code_action[winid].lightbulb_line = code_action[winid].lightbulb_line or 0 + code_action_req(action_vritual_call_back, diagnostics) +end + +return code_action diff --git a/lua/navigator/definition.lua b/lua/navigator/definition.lua new file mode 100644 index 0000000..0e64ec5 --- /dev/null +++ b/lua/navigator/definition.lua @@ -0,0 +1,101 @@ +local util = require "navigator.util" +local lsphelper = require "navigator.lspwrapper" +local locations_to_items = lsphelper.locations_to_items +local gui = require "navigator.gui" +local log = util.log +local TextView = require("guihua.textview") +-- callback for lsp definition, implementation and declaration handler +local function definition_hdlr(_, _, locations, _, bufnr) + -- log(locations) + if locations == nil or vim.tbl_isempty(locations) then + print "Definition not found" + return + end + if vim.tbl_islist(locations) then + if #locations > 1 then + local items = locations_to_items(locations) + gui.new_list_view({items = items, api = 'Definition'}) + else + vim.lsp.util.jump_to_location(locations[1]) + end + else + vim.lsp.util.jump_to_location(locations) + end +end + +local function def_preview(timeout_ms) + local method = "textDocument/definition" + local params = vim.lsp.util.make_position_params() + local result = vim.lsp.buf_request_sync(0, method, params, timeout_ms or 2000) + + if result == nil or vim.tbl_isempty(result) then + print("No result found: " .. method) + return nil + end + + local data = {} + -- result = {vim.tbl_deep_extend("force", {}, unpack(result))} + -- log("def-preview", result) + for key, value in pairs(result) do + if result[key] ~= nil then + table.insert(data, result[key].result[1]) + end + end + local range = data[1].targetRange or data[1].range + + local row = range.start.line + -- in case there are comments + row = math.max(row - 3, 1) + local delta = range.start.line - row + 1 + local uri = data[1].uri or data[1].targetUri + if not uri then + return + end + local bufnr = vim.uri_to_bufnr(uri) + if not vim.api.nvim_buf_is_loaded(bufnr) then + vim.fn.bufload(bufnr) + end + + local definition = vim.api.nvim_buf_get_lines(bufnr, row, range["end"].line + 12, false) + local def_line = vim.api.nvim_buf_get_lines(bufnr, range.start.line, range.start.line + 1, false) + for _ = 1, math.min(3, #definition), 1 do + if #definition[1] < 2 then + table.remove(definition, 1) + delta = delta - 1 + else + break + end + end + definition = vim.list_extend({"  " .. "Definition: "}, definition) + local filetype = vim.api.nvim_buf_get_option(bufnr, "filetype") + + -- TODO multiple resuts? + local opts = { + relative = "cursor", + style = "minimal", + ft = filetype, + data = definition, + enter = true + } + TextView:new(opts) + delta = delta + 1 -- header + local cmd = "normal! " .. tostring(delta) .. "G" + + vim.cmd(cmd) + vim.cmd('set cursorline') + if #def_line > 0 then + local niddle = require('guihua.util').add_escape(def_line[1]) + log(def_line[1], niddle) + vim.fn.matchadd("Search", niddle) + end + -- TODO: + -- https://github.com/oblitum/goyo.vim/blob/master/autoload/goyo.vim#L108-L135 +end + +vim.lsp.handlers["textDocument/definition"] = definition_hdlr +return { + definition_handler = definition_hdlr, + definition_preview = def_preview, + declaration_handler = definition_hdlr, + typeDefinition_handler = definition_hdlr +} diff --git a/lua/navigator/diagnostics.lua b/lua/navigator/diagnostics.lua new file mode 100644 index 0000000..44c3ed5 --- /dev/null +++ b/lua/navigator/diagnostics.lua @@ -0,0 +1,83 @@ +local gui = require "navigator.gui" +local diagnostic_list = {} +local log = require "navigator.util".log +diagnostic_list[vim.bo.filetype] = {} + +local diag_hdlr = function(err, method, result, client_id, br, config) + -- log(result) + vim.lsp.diagnostic.on_publish_diagnostics(err, method, result, client_id, br, config) + if err ~= nil then log(err, config) end + local cwd = vim.fn.getcwd(0) + local ft = vim.bo.filetype + if diagnostic_list[ft] == nil then + diagnostic_list[vim.bo.filetype] = {} + end + -- vim.lsp.diagnostic.clear(vim.fn.bufnr(), client.id, nil, nil) + + local uri = result.uri + if result and result.diagnostics then + local item_list = {} + + for _, v in ipairs(result.diagnostics) do + local item = v + item.filename = assert(vim.uri_to_fname(uri)) + item.display_filename = item.filename:gsub(cwd .. "/", "./", 1) + item.lnum = v.range.start.line + 1 + item.col = v.range.start.character + 1 + item.uri = uri + local head = " " + if v.severity > 1 then + head = " " + end + local bufnr = vim.uri_to_bufnr(uri) + vim.fn.bufload(bufnr) + local pos = v.range.start + local row = pos.line + local line = (vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false) or {""})[1] + item.text = head .. tostring(item.lnum) .. ": " .. line .. "  " .. v.message + table.insert(item_list, item) + end + -- local old_items = vim.fn.getqflist() + diagnostic_list[ft][uri] = item_list + end +end + +local M = {} +-- vim.lsp.handlers["textDocument/publishDiagnostics"] = +M.diagnostic_handler = + vim.lsp.with( + diag_hdlr, + { + -- Enable underline, use default values + underline = true, + -- Enable virtual text, override spacing to 0 + virtual_text = { + spacing = 0, + prefix = " " --' ,   + }, + -- Use a function to dynamically turn signs off + -- and on, using buffer local variables + signs = true, + -- Disable a feature + update_in_insert = false + } +) +M.show_diagnostic = function() + if diagnostic_list[vim.bo.filetype] ~= nil then + log(diagnostic_list[vim.bo.filetype]) + -- vim.fn.setqflist({}, " ", {title = "LSP", items = diagnostic_list[vim.bo.filetype]}) + local results = diagnostic_list[vim.bo.filetype] + local display_items = {} + for _, items in pairs(results) do + for _, it in pairs(items) do + table.insert(display_items, it) + end + end + log(display_items) + if #display_items > 0 then + gui.new_list_view({items = display_items, api = 'Diagnostic'}) + end + end +end + +return M diff --git a/lua/navigator/gui.lua b/lua/navigator/gui.lua new file mode 100644 index 0000000..ba4bbb9 --- /dev/null +++ b/lua/navigator/gui.lua @@ -0,0 +1,149 @@ +local M = {} +local ListView = require "guihua.listview" +local TextView = require "guihua.textview" +local View = require "guihua.view" +local util = require "navigator.util" +local log = require "navigator.util".log + +function M.new_preview(opts) + return TextView:new( + { + loc = "top_center", + rect = { + height = #opts.items + 4, + width = opts.width or 90, + pos_x = opts.pos_x or 0, + pos_y = opts.pos_y or 4 + }, + -- data = display_data, + relative = opts.relative, + data = opts.items, + syntax = opts.syntax, + enter = opts.enter or false + } + ) +end + +function M._preview_location(location, width, pos_x, pos_y) + local api = vim.api + local uri = location.targetUri or location.uri + if uri == nil then + log("invalid uri ", location) + return + end + local bufnr = vim.uri_to_bufnr(uri) + if not api.nvim_buf_is_loaded(bufnr) then + vim.fn.bufload(bufnr) + end + -- + local range = location.targetRange or location.range + local contents = api.nvim_buf_get_lines(bufnr, range.start.line, range["end"].line + 1, false) + -- + local syntax = api.nvim_buf_get_option(bufnr, "syntax") + if syntax == nil or #syntax < 1 then + syntax = api.nvim_buf_get_option(bufnr, "ft") + end + + log(syntax, contents) + local opts = {syntax = syntax, width = width, pos_x = pos_x or 0, pos_y = pos_y or 10} + opts.items = contents + log("syntax", opts.syntax) + return M.new_preview(opts) +end + +-- local bufnr, winnr =lsp.util.open_floating_preview(contents, syntax, {offset_x=30, offset_y=20}) +-- +-- vim.api.nvim_buf_set_var(bufnr, "lsp_floating", true) +-- return bufnr, winnr +-- + +function M.preview_file(filename, width, line, col, offset_x, offset_y) + log("file", filename, line, offset_x, offset_y) + if line >= 2 then + line = line - 2 + end + local loc = {uri = "file:///" .. filename, targetRange = {start = {line = line}}} + offset_x = offset_x or 0 + offset_y = offset_y or 6 + loc.targetRange["end"] = {line = line + 4} + return M._preview_location(loc, width, offset_x, offset_y) +end + +function M.preview_uri(uri, width, line, col, offset_x, offset_y) + log("uri", uri, line, offset_x, offset_y) + if line >= 2 then + line = line - 2 + end + offset_x = offset_x or 0 + offset_y = offset_y or 6 + local loc = {uri = uri, targetRange = {start = {line = line}}} + loc.targetRange["end"] = {line = line + 4} + return M._preview_location(loc, width, offset_x, offset_y) +end + +function M.new_list_view(opts) + local items = opts.items + local data = {} + if opts.rawdata then + data = items + else + data = require "guihua.util".aggregate_filename(items, opts) + end + local wwidth = vim.api.nvim_get_option("columns") + local width = opts.width or math.floor(wwidth * 0.8) + local wheight = math.floor(vim.api.nvim_get_option("lines") * 0.8) + local prompt = opts.prompt or false + if data and not vim.tbl_isempty(data) then + -- replace + if #data > 10 and opts.prompt == nil then + prompt = true + end + + local height = math.min(#data, math.floor(wheight / 2)) + local offset_y = height + if prompt then + offset_y = offset_y + 1 + end + return ListView:new( + { + loc = "top_center", + prompt = prompt, + relative = opts.relative, + style = opts.style, + api = opts.api, + rect = { + height = height, + width = width, + pos_x = 0, + pos_y = 0 + }, + -- data = display_data, + data = data, + on_confirm = opts.on_confirm or function(pos) + if pos == 0 then + pos = 1 + end + local l = data[pos] + if l.filename ~= nil then + util.open_file_at(l.filename, l.lnum) + end + end, + on_move = opts.on_move or function(pos) + if pos == 0 then + pos = 1 + end + local l = data[pos] + log("on move", pos, l.text or l, l.uri, l.filename) + -- todo fix + if l.uri ~= nil then + return M.preview_uri(l.uri, width, l.lnum, l.col, 0, offset_y) + else + return M.preview_file(l.filename, width, l.lnum, l.col, 0, offset_y) + end + end + } + ) + end +end + +return M diff --git a/lua/navigator/hierarchy.lua b/lua/navigator/hierarchy.lua new file mode 100644 index 0000000..933eb99 --- /dev/null +++ b/lua/navigator/hierarchy.lua @@ -0,0 +1,85 @@ +local gui = require "navigator.gui" +local util = require "navigator.util" +local log = util.log +local partial = util.partial +local lsphelper = require "navigator.lspwrapper" +local cwd = vim.fn.getcwd(0) +local M = {} + +local function call_hierarchy_handler(direction, err, _, result, _, _, error_message) + -- log('call_hierarchy') + if err ~= nil then + print("ERROR: " .. error_message) + return + end + + -- log("dir", direction, "result", result) + local items = {} + for _, call_hierarchy_call in pairs(result) do + local call_hierarchy_item = call_hierarchy_call[direction] + local kind = ' ' + if call_hierarchy_item.kind then + kind = require'navigator.lspclient.lspkind'.kind(call_hierarchy_item.kind) .. ' ' + end + for _, range in pairs(call_hierarchy_call.fromRanges) do + local filename = assert(vim.uri_to_fname(call_hierarchy_item.uri)) + local display_filename = filename:gsub(cwd .. "/", "./", 1) + table.insert( + items, + { + uri = call_hierarchy_item.uri, + filename = filename, + -- display_filename = filename:gsub(cwd .. "/", "./", 1), + display_filename = call_hierarchy_item.detail or display_filename, + text = kind .. call_hierarchy_item.name, + range = range, + lnum = range.start.line, + col = range.start.character + } + ) + end + end + return items +end + +local call_hierarchy_handler_from = partial(call_hierarchy_handler, "from") +local call_hierarchy_handler_to = partial(call_hierarchy_handler, "to") + +local function incoming_calls_handler(bang, err, method, result, client_id, bufnr) + local results = call_hierarchy_handler_from(err, method, result, client_id, bufnr, "Incoming calls not found") + gui.new_list_view({items = results, api = 'incomming'}) +end + +local function outgoing_calls_handler(bang, err, method, result, client_id, bufnr) + local results = call_hierarchy_handler_to(err, method, result, client_id, bufnr, "Outgoing calls not found") + + gui.new_list_view({items =results, api = 'outgoing'}) + --fzf_locations(bang, "", "Outgoing Calls", results, false) +end + + +function M.incoming_calls(bang, opts) + if not lsphelper.check_capabilities("call_hierarchy") then + return + end + + local params = vim.lsp.util.make_position_params() + util.call_sync("callHierarchy/incomingCalls", params, opts, partial(incoming_calls_handler, bang)) +end + +function M.outgoing_calls(bang, opts) + if not lsphelper.check_capabilities("call_hierarchy") then + return + end + + local params = vim.lsp.util.make_position_params() + util.call_sync("callHierarchy/outgoingCalls", params, opts, partial(outgoing_calls_handler, bang)) +end + +M.incoming_calls_call = partial(M.incoming_calls, 0) +M.outgoing_calls_call = partial(M.outgoing_calls, 0) + +M.incoming_calls_handler = partial(incoming_calls_handler, 0) +M.outgoing_calls_handler = partial(outgoing_calls_handler, 0) + +return M diff --git a/lua/navigator/hover.lua b/lua/navigator/hover.lua new file mode 100644 index 0000000..abd0201 --- /dev/null +++ b/lua/navigator/hover.lua @@ -0,0 +1,23 @@ +-- TODO: change background and use TextView? +local lsp = require("vim.lsp") +return { hover_handler = function(_, method, result) + vim.lsp.util.focusable_float( + method, + function() + if not (result and result.contents) then + return + end + local markdown_lines = lsp.util.convert_input_to_markdown_lines(result.contents) + markdown_lines = lsp.util.trim_empty_lines(markdown_lines) + if vim.tbl_isempty(markdown_lines) then + return + end + + local bnr, contents_winid, _, border_winid = vim.lsp.util.fancy_floating_markdown(markdown_lines) + lsp.util.close_preview_autocmd({"CursorMoved", "BufHidden", "InsertCharPre"}, contents_winid) + lsp.util.close_preview_autocmd({"CursorMoved", "BufHidden", "InsertCharPre"}, border_winid) + return bnr, contents_winid + end + ) + end +} diff --git a/lua/navigator/implementation.lua b/lua/navigator/implementation.lua new file mode 100644 index 0000000..4c8f36f --- /dev/null +++ b/lua/navigator/implementation.lua @@ -0,0 +1,38 @@ +local util = require "navigator.util" +local lsphelper = require "navigator.lspwrapper" +local gui = require "navigator.gui" +local M = {} +local location = require("guihua.location") +local partial = util.partial +local locations_to_items = lsphelper.locations_to_items +local log = util.log +-- dataformat should be same as reference +local function location_handler(err, _, locations, _, bufnr, error_message) + if err ~= nil then + print("ERROR: " .. tostring(err) .. error_message) + return + end + return locations_to_items(locations) +end + +local function implementation_handler(bang, err, method, result, client_id, bufnr) + local results = location_handler(err, method, result, client_id, bufnr, "Implementation not found") + gui.new_list_view({items =results, api = 'Implementation'}) +end + +function M.implementation(bang, opts) + if not lsphelper.check_capabilities("implementation") then + return + end + + local params = vim.lsp.util.make_position_params() + util.call_sync("textDocument/implementation", params, opts, partial(implementation_handler, bang)) +end + + + +M.implementation_call = partial(M.implementation, 0) + +M.implementation_handler = partial(implementation_handler, 0) + +return M diff --git a/lua/navigator/lspclient/attach.lua b/lua/navigator/lspclient/attach.lua new file mode 100644 index 0000000..5c1960e --- /dev/null +++ b/lua/navigator/lspclient/attach.lua @@ -0,0 +1,74 @@ +local vim, api = vim, vim.api +local lsp = require("vim.lsp") + +local util = require "navigator.util" +local log = util.log + +if not packer_plugins["nvim-lua/lsp-status.nvim"] or not packer_plugins["lsp-status.nvim"].loaded then + vim.cmd [[packadd lsp-status.nvim]] +end +local lsp_status = require("lsp-status") + +local diagnostic_map = function(bufnr) + local opts = {noremap = true, silent = true} + api.nvim_buf_set_keymap(bufnr, "n", "]O", ":lua vim.lsp.diagnostic.set_loclist()", opts) +end +local M = {} +local function documentHighlight() + api.nvim_exec( + [[ + hi LspReferenceRead cterm=bold gui=Bold ctermbg=yellow guibg=DarkOrchid3 + hi LspReferenceText cterm=bold gui=Bold ctermbg=red guibg=gray27 + hi LspReferenceWrite cterm=bold gui=Bold,Italic ctermbg=red guibg=MistyRose + augroup lsp_document_highlight + autocmd! * + autocmd CursorHold lua vim.lsp.buf.document_highlight() + autocmd CursorMoved lua vim.lsp.buf.clear_references() + augroup END + ]], + false + ) + vim.lsp.handlers["textDocument/documentHighlight"] = function(_, _, result, _) + if not result then + return + end + bufnr = api.nvim_get_current_buf() + vim.lsp.util.buf_clear_references(bufnr) + vim.lsp.util.buf_highlight_references(bufnr, result) + end +end + +M.on_attach = function(client, bufnr) + log("attaching") + if lsp_status ~= nil then + lsp_status.on_attach(client, bufnr) + end + require "lsp_signature".on_attach() + diagnostic_map(bufnr) + -- lspsaga + require "utils.highlight".add_highlight() + + api.nvim_buf_set_option(bufnr, "omnifunc", "v:lua.vim.lsp.omnifunc") + + -- https://github.com/fsouza + if client.resolved_capabilities.document_highlight then + documentHighlight() + end + + require("navigator.lspclient.mapping").setup({client = client, bufnr = bufnr, cap = client.resolved_capabilities}) + + vim.cmd [[packadd vim-illuminate]] + require "illuminate".on_attach(client) + require "utils.lspkind".init() + + local capabilities = vim.lsp.protocol.make_client_capabilities() + capabilities.textDocument.completion.completionItem.snippetSupport = true +end + + + +M.setup = function(cfg) + return M +end + +return M diff --git a/lua/navigator/lspclient/clients.lua b/lua/navigator/lspclient/clients.lua new file mode 100644 index 0000000..d4d57cb --- /dev/null +++ b/lua/navigator/lspclient/clients.lua @@ -0,0 +1,219 @@ +-- todo allow config passed in + +local lspconfig = nil +local lsp_status = nil +if not packer_plugins["nvim-lua/lsp-status.nvim"] or not packer_plugins["lsp-status.nvim"].loaded then + vim.cmd [[packadd lsp-status.nvim]] + lsp_status = require("lsp-status") + -- if lazyloading + vim.cmd [[packadd nvim-lspconfig]] + lspconfig = require "lspconfig" +end + +local cap = vim.lsp.protocol.make_client_capabilities() +local on_attach = require("navigator.lspclient.attach").on_attach +local lsp_status_cfg = { + status_symbol = "", + indicator_errors = "", --'', + indicator_warnings = "", --'', + indicator_info = "﯎", + --'', + indicator_hint = "💡", + indicator_ok = "", + --'✔️', + spinner_frames = {"⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"}, + select_symbol = function(cursor_pos, symbol) + if symbol.valuerange then + local value_range = { + ["start"] = { + character = 0, + line = vim.fn.byte2line(symbol.valuerange[1]) + }, + ["end"] = { + character = 0, + line = vim.fn.byte2line(symbol.valuerange[2]) + } + } + + return require("lsp-status.util").in_range(cursor_pos, value_range) + end + end +} + + +-- local gopls = {} +-- gopls["ui.completion.usePlaceholders"] = true + +local golang_setup = { + on_attach = on_attach, + capabilities = cap, + -- init_options = { + -- useplaceholders = true, + -- completeunimported = true + -- }, + message_level = vim.lsp.protocol.MessageType.Error, + cmd = { + "gopls" + + -- share the gopls instance if there is one already + -- "-remote=auto", + + --[[ debug options ]] + -- + --"-logfile=auto", + --"-debug=:0", + --"-remote.debug=:0", + --"-rpc.trace", + }, + settings = {}, + root_dir = function(fname) + local util = require("lspconfig").util + return util.root_pattern("go.mod", ".git")(fname) or util.path.dirname(fname) + end +} +local clang_cfg = { + cmd = { + "clangd", + "--background-index", + "--suggest-missing-includes", + "--clang-tidy", + "--header-insertion=iwyu" + }, + on_attach = function(client) + client.resolved_capabilities.document_formatting = true + on_attach(client) + end +} + +local sqls_cfg = { + on_attach = function(client, bufnr) + client.resolved_capabilities.execute_command = true + lsp_status.on_attach(client, bufnr) + require "utils.highlight".diagnositc_config_sign() + require "sqls".setup {picker = "telescope"} -- or default + end, + settings = { + cmd = {"sqls", "-config", "$HOME/.config/sqls/config.yml"}, + -- alterantively: + -- connections = { + -- { + -- driver = 'postgresql', + -- datasourcename = 'host=127.0.0.1 port=5432 user=postgres password=password dbname=user_db sslmode=disable', + -- }, + -- }, + workspace = { + library = { + -- this loads the `lua` files from nvim into the runtime. + [vim.fn.expand("$vimruntime/lua")] = true, + [vim.fn.expand("~/repos/nvim/lua")] = true + } + } + } +} +-- lua setup +local sumneko_root_path = vim.fn.expand("$HOME") .. "/github/sumneko/lua-language-server" +local sumneko_binary = vim.fn.expand("$HOME") .. "/github/sumneko/lua-language-server/bin/macOS/lua-language-server" + +local lua_cfg = { + cmd = {sumneko_binary, "-E", sumneko_root_path .. "/main.lua"}, + on_attach = on_attach, + settings = { + Lua = { + runtime = { + -- Tell the language server which version of Lua you're using (most likely LuaJIT in the case of Neovim) + version = "LuaJIT", + -- Setup your lua path + path = vim.split(package.path, ";") + }, + diagnostics = { + enable = true, + -- Get the language server to recognize the `vim` global + globals = { + "vim", + "describe", + "it", + "before_each", + "after_each", + "teardown", + "pending" + } + }, + workspace = { + -- Make the server aware of Neovim runtime files + library = { + [vim.fn.expand("$VIMRUNTIME/lua")] = true, + [vim.fn.expand("$VIMRUNTIME/lua/vim")] = true, + [vim.fn.expand("$VIMRUNTIME/lua/vim/lsp")] = true, + -- [vim.fn.expand("~/repos/nvim/lua")] = true + } + } + } + } +} + +local function lsp_status_setup() + local servers = { + "gopls", + "tsserver", + "flow", + "bashls", + "dockerls", + "pyls", + "sumneko_lua", + "vimls", + "html", + "jsonls", + "cssls", + "yamlls", + "clangd", + "sqls" + } + + for _, lspclient in ipairs(servers) do + if lsp_status ~= nl then + lsp_status.register_progress() + + lsp_status.config(lsp_status_cfg) + end + require "utils.highlight".diagnositc_config_sign() + require "utils.highlight".add_highlight() + end +end + +local function setup(user_opts) + if lspconfig == nil then + print("lsp-config need installed and enabled") + return + end + + lsp_status_setup() + + for _, lspclient in ipairs({"tsserver", "bashls", "flow", "dockerls", "vimls", "html", "jsonls", "cssls", "yamlls"}) do + lspconfig[lspclient].setup { + message_level = vim.lsp.protocol.MessageType.error, + log_level = vim.lsp.protocol.MessageType.error, + on_attach = on_attach, + capabilities = lsp_status.capabilities + } + end + + lspconfig.gopls.setup(golang_setup) + lspconfig.sqls.setup(sqls_cfg) + + require "lspconfig".sumneko_lua.setup(lua_cfg) + + lspconfig.clangd.setup(clang_cfg) + servers = { + "dockerls", + "bashls", + "rust_analyzer", + "pyls" + } + + for _, server in ipairs(servers) do + lspconfig[server].setup { + on_attach = on_attach + } + end +end +return {setup = setup, cap = cap} diff --git a/lua/navigator/lspclient/config.lua b/lua/navigator/lspclient/config.lua new file mode 100644 index 0000000..bb1fa71 --- /dev/null +++ b/lua/navigator/lspclient/config.lua @@ -0,0 +1,29 @@ +local lsp = require("vim.lsp") + +vim.cmd [[packadd lspsaga.nvim]] +local saga = require "lspsaga" +saga.init_lsp_saga() +M={} + + +local capabilities = vim.lsp.protocol.make_client_capabilities() +capabilities.textDocument.completion.completionItem.snippetSupport = true + +function M.reload_lsp() + vim.lsp.stop_client(vim.lsp.get_active_clients()) + vim.cmd [[edit]] +end + +function M.open_lsp_log() + local path = vim.lsp.get_log_path() + vim.cmd("edit " .. path) +end + +vim.cmd("command! -nargs=0 LspLog call v:lua.open_lsp_log()") +vim.cmd("command! -nargs=0 LspRestart call v:lua.reload_lsp()") + +print("loading lsp client") +local cfg = {} +require('lsp.clients').setup(cfg) +require('lsp.mappings').setup(cfg) +return M diff --git a/lua/navigator/lspclient/highlight.lua b/lua/navigator/lspclient/highlight.lua new file mode 100644 index 0000000..d20bc5f --- /dev/null +++ b/lua/navigator/lspclient/highlight.lua @@ -0,0 +1,48 @@ +local M = {} +local api = vim.api + +-- lsp sign          ﮻         ﯭ        ﳀ   +function M.diagnositc_config_sign() + vim.fn.sign_define('LspDiagnosticsSignError', {text='', texthl='LspDiagnosticsSignError',linehl='', numhl=''}) + vim.fn.sign_define('LspDiagnosticsSignWarning', {text='', texthl='LspDiagnosticsSignWarning', linehl='', numhl=''}) + vim.fn.sign_define('LspDiagnosticsSignInformation', {text='', texthl='LspDiagnosticsSignInformation', linehl='', numhl=''}) + vim.fn.sign_define('LspDiagnosticsSignHint', {text='💡', texthl='LspDiagnosticsSignHint', linehl='', numhl=''}) +end + +function M.add_highlight() + + -- lsp system default + api.nvim_command("hi! link LspDiagnosticsUnderlineError SpellBad") + api.nvim_command("hi! link LspDiagnosticsUnderlineWarning SpellRare") + api.nvim_command("hi! link LspDiagnosticsUnderlineInformation SpellRare") + api.nvim_command("hi! link LspDiagnosticsUnderlineHint SpellRare") + + + -- lspsaga + api.nvim_command("hi LspFloatWinBorder guifg=black") + api.nvim_command("hi def link TargetWord Error") + api.nvim_command("hi def link ReferencesCount Title") + api.nvim_command("hi def link DefinitionCount Title") + api.nvim_command("hi def link TargetFileName Comment") + api.nvim_command("hi def link DefinitionIcon Special") + api.nvim_command("hi def link ReferencesIcon Special") + api.nvim_command("hi def link HelpTitle Comment") + api.nvim_command("hi def link HelpItem Comment") + + + -- diagnostic for lspsaga, overwrite if needed + -- api.nvim_command("hi DiagnosticTruncateLine guifg=#6699cc gui=bold") + -- api.nvim_command("hi def link DiagnosticError Error") + -- api.nvim_command("hi def link DiagnosticWarning WarningMsg") + -- api.nvim_command("hi DiagnosticInformation guifg=#6699cc gui=bold") + -- api.nvim_command("hi DiagnosticHint guifg=#56b6c2 gui=bold") + + -- for nvim version 0.5 2020-11 + -- api.nvim_command("sign define LspDiagnosticsErrorSign text= texthl=LspDiagnosticsError linehl= numhl=") + -- api.nvim_command("sign define LspDiagnosticsWarningSign text= texthl=LspDiagnosticsWarning linehl= numhl=") + + api.nvim_command("hi def link DefinitionPreviewTitle Title") + +end + +return M diff --git a/lua/navigator/lspclient/init.lua b/lua/navigator/lspclient/init.lua new file mode 100644 index 0000000..f965627 --- /dev/null +++ b/lua/navigator/lspclient/init.lua @@ -0,0 +1,7 @@ +local M = {} +M.setup = function(cfg) + cfg = cfg or {} + require('navigator.lspclient.clients').setup(cfg) +end + +return M diff --git a/lua/navigator/lspclient/lspkind.lua b/lua/navigator/lspclient/lspkind.lua new file mode 100644 index 0000000..225136a --- /dev/null +++ b/lua/navigator/lspclient/lspkind.lua @@ -0,0 +1,38 @@ +local kind_symbols = { + Text = '', + Method = 'ƒ', + Function = '', + Constructor = '', + Field = 'ﴲ', + Variable = '', + Class = 'פּ', + Interface = '蘒', + Module = '', + Property = '', + Unit = '塞', + Value = '', + Enum = '了', + Keyword = '', + Snippet = '', + Color = '', + File = '', + Reference = '', + Folder = '', + EnumMember = '', + Constant = '', + Struct = ' ', + Event = 'ﳅ', + Operator ='', + TypeParameter = '', + Default = '', +} +local CompletionItemKind = {'', 'ƒ', '', '', 'ﴲ', '', '', 'ﰮ', '', '', '', '', '了', '', '﬌', '', '', '', '', '', '', '', 'ﳅ', '', '', ''} + +function lspkind.kind(kind) + -- require('vim.lsp.protocol').CompletionItemKind = {'', 'ƒ', '', '', 'ﴲ', '', '', 'ﰮ', '', '', '', '', '了', '', '﬌', '', '', '', '', '', '', '', 'ﳅ', '', '', ''} + return CompletionItemKind[kind] +end + + + +return lspkind diff --git a/lua/navigator/lspclient/mapping.lua b/lua/navigator/lspclient/mapping.lua new file mode 100644 index 0000000..56c133f --- /dev/null +++ b/lua/navigator/lspclient/mapping.lua @@ -0,0 +1,142 @@ +local log = require "navigator.util".log +local function set_keymap(...) + vim.api.nvim_set_keymap(...) +end + +local event_hdlrs = { + {ev = "BufWritePre", func = "diagnostic.set_loclist({open_loclist = false})"}, + {ev = "CursorHold", func = "document_highlight()"}, + {ev = "CursorHoldI", func = "document_highlight()"}, + {ev = "CursorMoved", func = "clear_references()"} +} + +local key_maps = { + {key = "gr", func = "references()"}, + {mode = "i", key = "", func = "signature_help()"}, + {key = "gs", func = "signature_help()"}, + {key = "g0", func = "document_symbol()"}, + {key = "gW", func = "workspace_symbol()"}, + {key = "", func = "definition()"}, + {key = "gD", func = "declaration()"}, + {key = "gp", func = "require('navigator.definition').definition_preview()"}, + + {key = "K", func = "hover()"}, + {key = "ga", mode = 'n', func = "code_action()"}, + {key = "ca", mode = 'v', func = "range_code_action()"}, + + {key = "re", func = "rename()"}, + {key = "gi", func = "incoming_calls()"}, + {key = "go", func = "outgoing_calls()"}, + {key = "gi", func = "implementation()"}, + {key = "gt", func = "type_definition()"}, + {key = "gL", func = "diagnostic.show_line_diagnostics()"}, + {key = "gG", func = "require('navigator.diagnostics').show_diagnostic()"}, + {key = "]d", func = "diagnostic.goto_next()"}, + {key = "[d", func = "diagnostic.goto_prev()"}, + + {key = "", func = "definition()"}, + {key = "g", func = "implementation()"} +} + +local function set_mapping(user_opts) + local opts = {noremap = true, silent = true} + user_opts = user_opts or {} + + local user_key = user_opts.keymaps or {} + local bufnr = user_opts.bufnr or 0 + local opts = { noremap=true, silent=true } + + local function buf_set_keymap(...) + vim.api.nvim_buf_set_keymap(bufnr, ...) + end + + -- local function buf_set_option(...) + -- vim.api.nvim_buf_set_option(bufnr, ...) + -- end + for _, v in pairs(user_key) do + local exists = false + for _, default in pairs(key_maps) do + if v.func == default.func then + default.key, exists = v.key, true + break + end + end + if not exists then + table.insert(key_maps, v) + end + end + + -- local key_opts = {vim.tbl_deep_extend("force", key_maps, unpack(result))} + for _, value in pairs(key_maps) do + local f = "lua vim.lsp.buf." .. value.func .. "" + if string.find(value.func, 'require') then + f = "lua " .. value.func .. "" + else if string.find(value.func, 'diagnostic') then + f = "lua vim.lsp." .. value.func .. "" + end + local k = value.key + local m = value.mode or "n" + buf_set_keymap(m, k, f, opts) + end + + -- format setup + + if user_opts.cap.document_formatting then + buf_set_keymap("n", "f", "lua vim.lsp.buf.formatting()", opts) + vim.cmd([[autocmd BufWritePre lua vim.lsp.buf.formatting()]]) + end + if user_opts.cap.document_range_formatting then + buf_set_keymap("v", "f", "lua vim.lsp.buf.range_formatting()", opts) + end +end + +local function set_event_handler(user_opts) + user_opts = user_opts or {} + local file_types = "c,cpp,h,go,python,vim,sh,javascript,html,css,lua,typescript,rust" + -- local format_files = "c,cpp,h,go,python,vim,javascript,typescript" --html,css, + vim.api.nvim_command [[augroup nvim_lsp_autos]] + vim.api.nvim_command [[autocmd!]] + + for _, value in pairs(event_hdlrs) do + local f = "" + if string.find(value.func, "diagnostic") then + f = "lua vim.lsp." .. value.func + else + f = "lua vim.lsp.buf." .. value.func + end + local cmd = "autocmd FileType " .. file_types .. " autocmd nvim_lsp_autos " .. value.ev .. " silent! " .. f + vim.api.nvim_command(cmd) + end + vim.api.nvim_command([[augroup END]]) +end + +local M = {} + +function M.setup(user_opts) + set_mapping(user_opts) + set_event_handler(user_opts) + local cap = user_opts.cap or {} + if cap.call_hierarchy then + vim.lsp.handlers["callHierarchy/incomingCalls"] = require "navigator.hierarchy".incoming_calls_handler + vim.lsp.handlers["callHierarchy/outgoingCalls"] = require "navigator.hierarchy".outgoing_calls_handler + end + + vim.lsp.handlers["textDocument/references"] = require "navigator.reference".reference_handler + vim.lsp.handlers["textDocument/codeAction"] = require "navigator.codeAction".code_action_handler + vim.lsp.handlers["textDocument/definition"] = require "navigator.definition".definition_handler + + if cap.declaration then + vim.lsp.handlers["textDocument/declaration"] = require "navigator.definition".declaration_handler + end + + vim.lsp.handlers["textDocument/typeDefinition"] = require "navigator.definition".typeDefinition_handler + vim.lsp.handlers["textDocument/implementation"] = require "navigator.implementation".implementation_handler + + vim.lsp.handlers["textDocument/documentSymbol"] = require "navigator.symbols".symbol_handler + vim.lsp.handlers["workspace/symbol"] = require "navigator.symbols".symbol_handler + vim.lsp.handlers["textDocument/publishDiagnostics"] = require'navigator.diagnostics'.diagnostic_handler + + -- vim.lsp.handlers["textDocument/hover"] = require 'navigator.hover'.hover_handler +end + +return M diff --git a/lua/navigator/lspwrapper.lua b/lua/navigator/lspwrapper.lua new file mode 100644 index 0000000..b9ed26a --- /dev/null +++ b/lua/navigator/lspwrapper.lua @@ -0,0 +1,128 @@ +local M = {} +local util = require "navigator.util" +local lsp = require "vim.lsp" +local log = require "navigator.util".log +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 + +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 + goto continue + end + end + + ::continue:: + if supported_client then + return true + else + if #clients == 0 then + print("LSP: no client attached") + else + print("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) + + handler(err, method, extract_result(results_lsp), nil, nil) +end + +function M.call_async(method, params, handler) + params = params or {} + local callback = function(...) + util.show(...) + handler(...) + end + local results_lsp, canceller = lsp.buf_request(0, method, params, callback) + return results_lsp, canceller + -- handler(err, method, extract_result(results_lsp), nil, nil) +end + +function M.locations_to_items(locations) + local cwd = vim.fn.getcwd(0) + 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, loc 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 .. "/", "./", 1) + item.display_filename = filename or item.filename + item.rpath = util.get_relative_path(cwd, item.filename) + table.insert(items, item) + end + + -- insert data into lines and loc + -- for i, loc in ipairs(items) do + -- log(items[i], locations[i]) + -- local filename = loc.filename:gsub(cwd .. "/", "./", 1) + -- items[i].uri = locations[i].uri + -- items[i].range = locations[i].range + -- items[i].filename = assert(vim.uri_to_fname(loc.uri)) + -- items[i].display_filename = filename or items[i].filename + -- items[i].rpath = util.get_relative_path(cwd, loc.filename) + -- log(items[i], locations[i]) + -- end + return items +end + +return M diff --git a/lua/navigator/protocal.json b/lua/navigator/protocal.json new file mode 100644 index 0000000..6922d88 --- /dev/null +++ b/lua/navigator/protocal.json @@ -0,0 +1,453 @@ +--[[ -- incomming/outgoing +dir from result { { + from = { + detail = "command-line-arguments • interface.go", + kind = 12, + name = "m2", + range = { + end = { + character = 7, + line = 39 + }, + start = { + character = 5, + line = 39 + } + }, + selectionRange = { + end = { + character = 7, + line = 39 + }, + start = { + character = 5, + line = 39 + } + }, + uri = "file:///Users/ray.xu/lsp-test/go/interface.go" + }, + fromRanges = { { + end = { + character = 8, + line = 40 + }, + start = { + character = 1, + line = 40 + } + } } + }, { + from = { + detail = "command-line-arguments • interface.go", + kind = 12, + name = "main", + range = { + end = { + character = 9, + line = 43 + }, + start = { + character = 5, + line = 43 + } + }, + selectionRange = { + end = { + character = 9, + line = 43 + }, + start = { + character = 5, + line = 43 + } + }, + uri = "file:///Users/ray.xu/lsp-test/go/interface.go" + }, + fromRanges = { { + end = { + character = 11, + line = 47 + }, + start = { + character = 4, + line = 47 + } + }, { + end = { + character = 11, + line = 48 + }, + start = { + character = 4, + line = 48 + } + } } + } } + +--]] + + + + + +-- [[ locations/reference from lsp +{ { + range = { + ["end"] = { + character = 20, + line = 26 + }, + start = { + character = 16, + line = 26 + } + }, + uri = "file:///Users/ray.xu/lsp-test/go/interface.go" + }, { + range = { + ["end"] = { + character = 22, + line = 35 + }, + start = { + character = 18, + line = 35 + } + }, + uri = "file:///Users/ray.xu/lsp-test/go/interface.go" + } } +--]] + + +-- definition + +definition.lua:9: { { + range = { + end = { + character = 12, + line = 33 + }, + start = { + character = 5, + line = 33 + } + }, + uri = "file:///Users/ray.xu/lsp-test/go/interface.go" + } } + + + -- def preview + + def-preview { + [3] = { + result = { { + range = { + end = { + character = 12, + line = 33 + }, + start = { + character = 5, + line = 33 + } + }, + uri = "file:///Users/ray.xu/lsp-test/go/interface.go" + } } + } +} + + +-- symbol +{ { + containerName = "command-line-arguments", + kind = 11, + location = { + range = { + end = { + character = 13, + line = 7 + }, + start = { + character = 5, + line = 7 + } + }, + uri = "file:///Users/ray.xu/lsp-test/go/interface.go" + }, + name = "command-line-arguments.geometry" + }, { + containerName = "command-line-arguments", + kind = 23, + location = { + range = { + end = { + character = 9, + line = 12 + }, + start = { + character = 5, + line = 12 + } + }, + uri = "file:///Users/ray.xu/lsp-test/go/interface.go" + }, + name = "command-line-arguments.rect" + }, { + containerName = "command-line-arguments", + kind = 12, + location = { + range = { + end = { + character = 9, + line = 43 + }, + start = { + character = 5, + line = 43 + } + }, + uri = "file:///Users/ray.xu/lsp-test/go/interface.go" + }, + name = "command-line-arguments.main" + }, { + containerName = "command-line-arguments", + kind = 6, + location = { + range = { + end = { + character = 8, + line = 8 + }, + start = { + character = 4, + line = 8 + } + }, + uri = "file:///Users/ray.xu/lsp-test/go/interface.go" + }, + name = "command-line-arguments.geometry.area" + }, { + containerName = "command-line-arguments", + kind = 8, + location = { + range = { + end = { + character = 9, + line = 13 + }, + start = { + character = 4, + line = 13 + } + }, + uri = "file:///Users/ray.xu/lsp-test/go/interface.go" + }, + name = "command-line-arguments.rect.width" + }} + + +{ + diagnostics = { { + code = "UnusedVar", + codeDescription = { + href = "https://pkg.go.dev/golang.org/x/tools/internal/typesinternal#UnusedVar" + }, + message = "d declared but not used", + range = { + end = { + character = 5, + line = 46 + }, + start = { + character = 4, + line = 46 + } + }, + severity = 1, + source = "compiler" + }, { + code = "WrongArgCount", + codeDescription = { + href = "https://pkg.go.dev/golang.org/x/tools/internal/typesinternal#WrongArgCount" + }, + message = "missing argument in conversion to circle", + range = { + end = { + character = 17, + line = 46 + }, + start = { + character = 9, + line = 46 + } + }, + severity = 1, + source = "compiler" + } }, + uri = "file:///Users/ray.xu/lsp-test/go/interface.go" +} + + +-- range actions + { { + edit = { + documentChanges = { { + edits = { { + newText = '\nimport (\n\t"fmt"\n\t"log"\n)\n', + range = { + end = { + character = 0, + line = 1 + }, + start = { + character = 0, + line = 1 + } + } + } }, + textDocument = { + uri = "file:///Users/ray.xu/lsp_test/go/ref.go", + version = 0 + } + } } + }, + kind = "source.organizeImports", + title = "Organize Imports" + }, { + command = { + arguments = { { + Fix = "undeclared_name", + Range = { + end = { + character = 20, + line = 4 + }, + start = { + character = 16, + line = 4 + } + }, + URI = "file:///Users/ray.xu/lsp_test/go/ref.go" + } }, + command = "gopls.apply_fix", + title = "undeclared name: rect" + }, + diagnostics = { { + code = "UndeclaredName", + codeDescription = { + href = "https://pkg.go.dev/golang.org/x/tools/internal/typesinternal#UndeclaredName" + }, + message = "undeclared name: rect", + range = { + end = { + character = 20, + line = 4 + }, + start = { + character = 16, + line = 4 + } + }, + severity = 1, + source = "compiler", + tags = { 1 } + } }, + edit = {}, + kind = "quickfix", + title = "undeclared name: rect" + }, { + command = { + arguments = { { + Fix = "extract_function", + Range = { + end = { + character = 16, + line = 6 + }, + start = { + character = 0, + line = 4 + } + }, + URI = "file:///Users/ray.xu/lsp_test/go/ref.go" + } }, + command = "gopls.apply_fix", + title = "Extract to function" + }, + edit = {}, + kind = "refactor.extract", + title = "Extract to function" + } } + + + + -- code action + + { { + diagnostics = { { + code = "UndeclaredName", + codeDescription = { + href = "https://pkg.go.dev/golang.org/x/tools/internal/typesinternal#UndeclaredName" + }, + message = "undeclared name: log", + range = { + end = { + character = 4, + line = 6 + }, + start = { + character = 1, + line = 6 + } + }, + severity = 1, + source = "compiler" + } }, + edit = { + documentChanges = { { + edits = { { + newText = '\nimport "log"\n', + range = { + end = { + character = 0, + line = 1 + }, + start = { + character = 0, + line = 1 + } + } + } }, + textDocument = { + uri = "file:///Users/ray.xu/lsp_test/go/ref.go", + version = 0 + } + } } + }, + kind = "quickfix", + title = 'Add import: "log"' + }, { + edit = { + documentChanges = { { + edits = { { + newText = '\nimport (\n\t"fmt"\n\t"log"\n)\n', + range = { + end = { + character = 0, + line = 1 + }, + start = { + character = 0, + line = 1 + } + } + } }, + textDocument = { + uri = "file:///Users/ray.xu/lsp_test/go/ref.go", + version = 0 + } + } } + }, + kind = "source.organizeImports", + title = "Organize Imports" + } } diff --git a/lua/navigator/reference.lua b/lua/navigator/reference.lua new file mode 100644 index 0000000..d27e388 --- /dev/null +++ b/lua/navigator/reference.lua @@ -0,0 +1,32 @@ +local util = require "navigator.util" +local lsphelper = require "navigator.lspwrapper" +local gui = require "navigator.gui" +local log = require "navigator.util".log +-- local log = util.log +-- local partial = util.partial +-- local cwd = vim.fn.getcwd(0) +-- local lsphelper = require "navigator.lspwrapper" +local locations_to_items = lsphelper.locations_to_items + +--vim.api.nvim_set_option("navtator_options", {width = 90, height = 60, location = require "navigator.location".center}) +-- local options = vim.g.navtator_options or {width = 60, height = 40, location = location.center} + + +local function ref_hdlr(arg1, api, locations, num, bufnr) + local opts = {} + + -- log("arg1", arg1) + -- log(api) + -- log("num", num) + -- log("bfnr", bufnr) + + if locations == nil or vim.tbl_isempty(locations) then + print "References not found" + return + end + log(locations) + local items = locations_to_items(locations) + gui.new_list_view({items = items, api = 'Reference'}) +end + +return { reference_handler = ref_hdlr } diff --git a/lua/navigator/symbols.lua b/lua/navigator/symbols.lua new file mode 100644 index 0000000..8aaa51d --- /dev/null +++ b/lua/navigator/symbols.lua @@ -0,0 +1,115 @@ +local gui = require "navigator.gui" +local M = {} +local log = require "navigator.util".log +local lsphelper = require "navigator.lspwrapper" +local locations_to_items = lsphelper.locations_to_items + +function M.document_symbols(opts) + opts = opts or {} + local params = vim.lsp.util.make_position_params() + params.context = {includeDeclaration = true} + params.query = "" + local results_lsp = vim.lsp.buf_request_sync(0, "textDocument/documentSymbol", params, opts.timeout or 10000) + local locations = {} + log(results_lsp) + for _, server_results in pairs(results_lsp) do + if server_results.result then + vim.list_extend(locations, vim.lsp.util.symbols_to_items(server_results.result) or {}) + end + end + local lines = {} + + for _, loc in ipairs(locations) do + table.insert(lines, string.format("%s:%s:%s", loc.filename, loc.lnum, loc.text)) + end + local cmd = table.concat(lines, "\n") + if #lines > 0 then + gui.new_list_view({data = lines}) + else + print("symbols not found") + end +end + +function M.workspace_symbols(opts) + opts = opts or {} + local params = vim.lsp.util.make_position_params() + params.context = {includeDeclaration = true} + params.query = "" + local results_lsp = vim.lsp.buf_request_sync(0, "workspace/symbol", params, opts.timeout or 10000) + + log(results_lsp) + local locations = {} + for _, server_results in pairs(results_lsp) do + if server_results.result then + vim.list_extend(locations, vim.lsp.util.symbols_to_items(server_results.result) or {}) + end + end + local lines = {} + + for _, loc in ipairs(locations) do + table.insert(lines, string.format("%s:%s:%s", loc.filename, loc.lnum, loc.text)) + end + if #lines > 0 then + gui.new_list_view({data = lines}) + else + print("symbols not found") + end +end + +function M.symbol_handler(_, _, result, _, bufnr) + if not result or vim.tbl_isempty(result) then + print("symbol not found") + return + end + -- log(result) + local locations = {} + for i = 1, #result do + local item = result[i].location + item.kind = result[i].kind + item.containerName = result[i].containerName + item.name = result[i].name + item.text = result[i].name + if #item.containerName > 0 then + item.text = item.text:gsub(item.containerName, '', 1) + end + table.insert(locations, item) + end + local items = locations_to_items(locations) + gui.new_list_view({items = items, prompt = true}) + + -- if locations == nil or vim.tbl_isempty(locations) then + -- print "References not found" + -- return + -- end + -- local items = locations_to_items(locations) + -- gui.new_list_view({items = items}) + -- local filename = vim.api.nvim_buf_get_name(bufnr) + -- local items = vim.lsp.util.symbols_to_items(result, bufnr) + -- local data = {} + -- for i, item in pairs(action.items) do + -- data[i] = item.text + -- if filename ~= item.filename then + -- local cwd = vim.fn.getcwd(0) .. "/" + -- local add = util.get_relative_path(cwd, item.filename) + -- data[i] = data[i] .. " - " .. add + -- end + -- item.text = nil + -- end + -- opts.data = data + -- action.popup = popfix:new(opts) + -- if not action.popup then + -- action.items = nil + -- end + -- if action.popup.list then + -- util.setFiletype(action.popup.list.buffer, "lsputil_symbols_list") + -- end + -- if action.popup.preview then + -- util.setFiletype(action.popup.preview.buffer, "lsputil_symbols_preview") + -- end + -- if action.popup.prompt then + -- util.setFiletype(action.popup.prompt.buffer, "lsputil_symbols_prompt") + -- end + -- opts.data = nil +end + +return M diff --git a/lua/navigator/util.lua b/lua/navigator/util.lua new file mode 100644 index 0000000..1b52134 --- /dev/null +++ b/lua/navigator/util.lua @@ -0,0 +1,231 @@ +-- retreives data form file +-- and line to highlight +-- Some of function copied from https://github.com/RishabhRD/nvim-lsputils + +local M = { + log_path = vim.lsp.get_log_path() +} +function M.get_data_from_file(filename, startLine) + local displayLine + if startLine < 3 then + displayLine = startLine + startLine = 0 + else + startLine = startLine - 2 + displayLine = 2 + end + local uri = "file:///" .. filename + local bufnr = vim.uri_to_bufnr(uri) + vim.fn.bufload(bufnr) + local data = vim.api.nvim_buf_get_lines(bufnr, startLine, startLine + 8, false) + if data == nil or vim.tbl_isempty(data) then + startLine = nil + else + local len = #data + startLine = startLine + 1 + for i = 1, len, 1 do + data[i] = startLine .. " " .. data[i] + startLine = startLine + 1 + end + end + return { + data = data, + line = displayLine + } +end + +function M.get_base(path) + local len = #path + for i = len, 1, -1 do + if path:sub(i, i) == "/" then + local ret = path:sub(i + 1, len) + return ret + end + end +end + +local function getDir(path) + local data = {} + local len = #path + if len <= 1 then return nil end + local last_index = 1 + for i = 2, len do + local cur_char = path:sub(i,i) + if cur_char == '/' then + local my_data = path:sub(last_index + 1, i - 1) + table.insert(data, my_data) + last_index = i + end + end + return data +end + +function M.get_relative_path(base_path, my_path) + local base_data = getDir(base_path) + local my_data = getDir(my_path) + local base_len = #base_data + local my_len = #my_data + + if base_len > my_len then + return my_path + end + + if base_data[1] ~= my_data[1] then + return my_path + end + + local cur = 0 + for i = 1, base_len do + if base_data[i] ~= my_data[i] then + break + end + cur = i + end + local data = "" + for i = cur + 1, my_len do + data = data .. my_data[i] .. "/" + end + data = data .. M.get_base(my_path) + return data +end + +local default_config = { + plugin = "navigator", + use_console = false, + use_file = true, + level = "info" +} + +M._log = require('guihua.log').new({level='debug'}, true) + +-- add log to you lsp.log +M.log = M._log.info +M.verbose = M._log.debug + +function M.fmt(...) + M._log.fmt_info(...) +end + +function M.split(inputstr, sep) + if sep == nil then + sep = "%s" + end + local t = {} + for str in string.gmatch(inputstr, "([^" .. sep .. "]+)") do + table.insert(t, str) + end + return t +end + +function M.trim_space(s) + return s:match("^%s*(.-)%s*$") +end + +function M.quickfix_extract(line) + -- check if it is a line of file pos been selected + local split = M.split + line = M.trim_space(line) + local sep = split(line, " ") + if #sep < 2 then + M.log(line) + return nil + end + sep = split(sep[1], ":") + if #sep < 3 then + M.log(line) + return nil + end + local location = {uri = "file:///" .. sep[1], range = {start = {line = sep[2] - 3 > 0 and sep[2] - 3 or 1}}} + location.range["end"] = {line = sep[2] + 15} + return location +end + +function M.getArgs(inputstr) + local sep = "%s" + local t = {} + local cmd + for str in string.gmatch(inputstr, "([^" .. sep .. "]+)") do + if not cmd then + cmd = str + else + table.insert(t, str) + end + end + return cmd, t +end + +function M.p(t) + print(vim.inspect(t)) +end + +function M.printError(msg) + vim.cmd("echohl ErrorMsg") + vim.cmd(string.format([[echomsg '%s']], msg)) + vim.cmd("echohl None") +end + +function M.reload() + vim.lsp.stop_client(vim.lsp.get_active_clients()) + vim.cmd [[edit]] +end + +function M.open_log() + local path = vim.lsp.get_log_path() + vim.cmd("edit " .. path) +end + +function table.pack(...) + return {n = select("#", ...), ...} +end + +function M.show(...) + local string = "" + + local args = table.pack(...) + + for i = 1, args.n do + string = string .. tostring(args[i]) .. "\t" + end + + return string .. "\n" +end + +function M.split(s, sep) + local fields = {} + + local sep = sep or " " + local pattern = string.format("([^%s]+)", sep) + string.gsub( + s, + pattern, + function(c) + fields[#fields + 1] = c + end + ) + + return fields +end + +M.open_file = function(filename) + vim.api.nvim_command(string.format("e! %s", filename)) +end + +M.open_file_at = function(filename, line) + vim.api.nvim_command(string.format("e! +%s %s", line, filename)) +end + +function M.exists(var) + for k, _ in pairs(_G) do + if k == var then + return true + end + end +end + +function M.partial(func, arg) + return (function(...) + return func(arg, ...) + end) +end + +return M diff --git a/plugin/navigator.vim b/plugin/navigator.vim new file mode 100644 index 0000000..cc6d50d --- /dev/null +++ b/plugin/navigator.vim @@ -0,0 +1,24 @@ +" built upon popfix api(https://github.com/RishabhRD/popfix) +" for parameter references see popfix readme. + +if exists('g:loaded_navigator_lsp') | finish | endif + +let s:save_cpo = &cpo +set cpo&vim + +if ! exists('g:navigator_lsp_location_opts') + let g:navigator_lsp_location_opts = v:null +endif + +if ! exists('g:navigator_lsp_symbols_opts') + let g:navigator_lsp_symbols_opts = v:null +endif + +if ! exists('g:navigator_lsp_codeaction_opts') + let g:navigator_lsp_codeaction_opts = v:null +endif + +let &cpo = s:save_cpo +unlet s:save_cpo + +let g:loaded_navigator_lsp = 1