diff --git a/README.md b/README.md index 38e9d7d..f43c1fc 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,8 @@ _Screenshot is showing the Snacks picker._ ## Features -- Browse and search Go standard library packages and project packages. +- Search and browse docs for Go standard library packages and project packages. +- Go to definition capability. - Syntax highlighting for Go documentation. - Optionally leverage [`stdsym`](https://github.com/lotusirous/gostdsym) for symbols searching. @@ -89,6 +90,8 @@ provided: - `:GoDoc` - Open picker and search packages. - `:GoDoc ` - Directly open documentation for the specified package or symbol. +- In Normal mode, press `gd` to go package definition (only supported by Snacks + and Telescope pickers). For fzf-lua, the keymap is `ctrl-s`. > [!WARNING] > @@ -287,6 +290,7 @@ All adapters must implement the interface of `GoDocAdapter`: --- @field get_items fun(): string[] Function that returns a list of available items --- @field get_content fun(choice: string): string[] Function that returns the content --- @field get_syntax_info fun(): GoDocSyntaxInfo Function that returns syntax info +--- @field goto_definition? fun(choice: string, picker_gotodef_fun: fun()?): nil Function that returns the definition location --- @field health? fun(): GoDocHealthCheck[] Optional health check function ``` @@ -299,6 +303,7 @@ The `opts` which can be passed into an adapter (by the user) is implemented by --- @field get_items? fun(): string[] Override the get_items function --- @field get_content? fun(choice: string): string[] Override the get_content function --- @field get_syntax_info? fun(): GoDocSyntaxInfo Override the get_syntax_info function +--- @field goto_definition? fun(choice: string, picker_gotodef_fun: fun()?): nil Override the get_definition function --- @field health? fun(): GoDocHealthCheck[] Override the health check function --- @field [string] any Other adapter-specific options ``` diff --git a/lua/godoc/adapters/go.lua b/lua/godoc/adapters/go.lua index d598f21..1cda6fb 100644 --- a/lua/godoc/adapters/go.lua +++ b/lua/godoc/adapters/go.lua @@ -84,6 +84,94 @@ local function get_packages() return all_packages end +--- @param item string +--- @param picker_gotodef_fun fun() +local function goto_definition(item, picker_gotodef_fun) + -- write temp file + local content = {} + local cursor_pos = {} + -- check if the chosen item contains a symbol + local is_symbol = string.match(item, "%.%a[%w_]*$") ~= nil + if is_symbol then + -- an import with symbol is passed, e.g. archive/tar.FileInfoNames + -- + -- extract the import path (archive/tar) + local import_path = item:match("^(.-)%.") + -- extract the package name, which comes after the last slash (e.g. tar) + local package_name = import_path:match("([^/]+)$") + -- extract the symbol name, which comes after the last dot (e.g. FileInfoNames) + local symbol = item:match("([^%.]+)$") + -- the contents of the go file, which contains imports to the package and the symbol as well as the fmt package + local line = ' fmt.Printf("%v", ' .. package_name .. "." .. symbol .. ")" + content = { + "package main", + "", + "import (", + ' "' .. import_path .. '"', + ' "fmt"', + ")", + "", + "func main() {", + line, + "}", + } + -- put the position/cursor on the symbol, e.g. on the 'F' of FileInfoNames + cursor_pos = { 9, line:find(symbol) + 1 } + else + -- a package name is passed, e.g. "archive/tar" + content = { + "package main", + "", + 'import "' .. item .. '"', + } + cursor_pos = { 3, 9 } + end + local now = os.time() + local filename = "godoc_" .. now .. ".go" + local tempfile = vim.fn.getcwd() .. "/" .. filename + local go_mod_filepath = vim.fn.findfile("go.mod", vim.fn.getcwd() .. ";") + local go_mod_dir = vim.fn.fnamemodify(go_mod_filepath, ":p:h") + if go_mod_dir == "" then + vim.notify("Failed to find go.mod file, can only gotodef on std lib", vim.log.levels.WARN) + else + tempfile = go_mod_dir .. "/" .. filename + end + vim.fn.writefile(content, tempfile) + + -- Open the temp file in the current (godoc-managed) buffer + vim.cmd("silent! e " .. tempfile) + local window = vim.api.nvim_get_current_win() + local buf = vim.api.nvim_get_current_buf() + + -- Set buffer options + vim.api.nvim_set_option_value("filetype", "go", { buf = buf }) + vim.api.nvim_set_option_value("bufhidden", "wipe", { buf = buf }) + vim.diagnostic.enable(false, { bufnr = buf }) + + -- Wait until LSP has attached, can be queried and returns a client_id + local client_id = nil + local maxretries = 50 + while client_id == nil and maxretries >= 0 do + for _, client in ipairs(vim.lsp.get_clients({ name = "gopls", bufnr = buf })) do + client_id = client.id + break + end + vim.wait(50) + maxretries = maxretries - 1 + end + + -- Position cursor at the right spot + vim.api.nvim_win_set_cursor(window, cursor_pos) + + -- Execute goto definition in the new window + vim.api.nvim_win_call(window, function() + picker_gotodef_fun() + end) + + -- Delete the temp file on disk + vim.fn.delete(tempfile) +end + local function health() --- @type GoDocHealthCheck[] local checks = {} @@ -217,6 +305,9 @@ function M.setup(opts) language = "go", } end, + goto_definition = function(choice, picker_gotodef_fun) + return goto_definition(choice, picker_gotodef_fun) + end, health = health, } end diff --git a/lua/godoc/adapters/init.lua b/lua/godoc/adapters/init.lua index a79ebb2..90be618 100644 --- a/lua/godoc/adapters/init.lua +++ b/lua/godoc/adapters/init.lua @@ -44,6 +44,7 @@ function M.validate_adapter(adapter) { name = "get_items", type = "function" }, { name = "get_content", type = "function" }, { name = "get_syntax_info", type = "function" }, + { name = "goto_definition", type = "function" }, } for _, field in ipairs(required_fields) do @@ -70,6 +71,7 @@ function M.override_adapter(default_adapter, user_opts) get_items = user_opts.get_items or default_adapter.get_items, get_content = user_opts.get_content or default_adapter.get_content, get_syntax_info = user_opts.get_syntax_info or default_adapter.get_syntax_info, + goto_definition = user_opts.goto_definition or default_adapter.goto_definition, health = user_opts.health or default_adapter.health, } end diff --git a/lua/godoc/init.lua b/lua/godoc/init.lua index 2d96dfa..96e7ef8 100644 --- a/lua/godoc/init.lua +++ b/lua/godoc/init.lua @@ -82,6 +82,16 @@ local function configure_adapters(config) return configured_adapters end +--- Open window based on split type +--- @param type 'split' | 'vsplit' +local function open_window(type) + if type == "split" or type == "vsplit" then + vim.cmd(type) + else + vim.notify("Invalid window type: " .. type, vim.log.levels.ERROR) + end +end + -- Set up the plugin with user config --- @param opts? GoDocConfig function M.setup(opts) @@ -104,9 +114,15 @@ function M.setup(opts) local picker = pickers.get_picker(M.config.picker.type) if picker then ---@type GoDocPicker - picker.show(adapter, M.config, function(choice) - if choice then - M.show_documentation(adapter, choice) + picker.show(adapter, M.config, function(data) + if data.choice then + if data.type == "show_documentation" then + open_window(M.config.window.type) + M.show_documentation(adapter, data.choice) + elseif data.type == "goto_definition" then + open_window(M.config.window.type) + M.goto_definition(adapter, data.choice, picker.goto_definition) + end end end) else @@ -130,15 +146,6 @@ function M.show_documentation(adapter, item) vim.api.nvim_set_option_value("modifiable", false, { buf = buf }) vim.api.nvim_set_option_value("filetype", adapter.get_syntax_info().filetype, { buf = buf }) - -- Open window based on config - if M.config.window.type == "split" then - vim.cmd("split") - elseif M.config.window.type == "vsplit" then - vim.cmd("vsplit") - else - vim.notify("Invalid window type: " .. M.config.window.type, vim.log.levels.ERROR) - end - vim.api.nvim_set_current_buf(buf) -- Set up keymaps for the documentation window @@ -147,4 +154,19 @@ function M.show_documentation(adapter, item) vim.keymap.set("n", "", ":close", opts) end +--- Go to definition on chosen item +--- @param adapter GoDocAdapter +--- @param choice string The chosen item (package, symbol) +--- @param picker_gotodef_fun fun()? The picker's goto_definition function +--- @return nil +function M.goto_definition(adapter, choice, picker_gotodef_fun) + if not picker_gotodef_fun then + vim.notify( + "Picker does not implement a function which can be used for showing definitions", + vim.log.levels.WARN + ) + end + adapter.goto_definition(choice, picker_gotodef_fun) +end + return M diff --git a/lua/godoc/pickers/fzf_lua.lua b/lua/godoc/pickers/fzf_lua.lua index 9dc74c8..4ffa447 100644 --- a/lua/godoc/pickers/fzf_lua.lua +++ b/lua/godoc/pickers/fzf_lua.lua @@ -3,7 +3,7 @@ local M = {} --- @param adapter GoDocAdapter --- @param config GoDocConfig ---- @param callback fun(choice: string|nil) +--- @param callback fun(choice: GoDocCallbackData) function M.show(adapter, config, callback) local core = require("fzf-lua.core") local opts = { @@ -12,7 +12,11 @@ function M.show(adapter, config, callback) debug = false, actions = { ["default"] = function(selected, _) - callback(table.concat(selected, "")) + callback({ type = "show_documentation", choice = table.concat(selected, "") }) + end, + -- TODO: fzf lua doesn't accept 'gd' as a keymap + ["ctrl-s"] = function(selected, _) + callback({ type = "goto_definition", choice = table.concat(selected, "") }) end, }, } @@ -24,4 +28,9 @@ function M.show(adapter, config, callback) core.fzf_exec(adapter.get_items(), opts) end +--- @return nil +M.goto_definition = function() + require("fzf-lua").lsp_definitions() +end + return M diff --git a/lua/godoc/pickers/mini.lua b/lua/godoc/pickers/mini.lua index 1423295..0c858c5 100644 --- a/lua/godoc/pickers/mini.lua +++ b/lua/godoc/pickers/mini.lua @@ -3,7 +3,7 @@ local M = {} --- @param adapter GoDocAdapter --- @param config GoDocConfig ---- @param callback fun(choice: string|nil) +--- @param callback fun(choice: GoDocCallbackData) function M.show(adapter, config, callback) local minipick = require("mini.pick") @@ -20,7 +20,7 @@ function M.show(adapter, config, callback) vim.api.nvim_set_option_value("filetype", syntax_info.filetype, { buf = buf_id }) end, choose = function(item) - callback(item) + callback({ type = "show_documentation", choice = item }) end, }, } @@ -32,4 +32,7 @@ function M.show(adapter, config, callback) minipick.start(opts) end +-- NOTE: goto definition is not supported in mini picker +M.goto_definition = nil + return M diff --git a/lua/godoc/pickers/native.lua b/lua/godoc/pickers/native.lua index cfb0f9a..b3d4613 100644 --- a/lua/godoc/pickers/native.lua +++ b/lua/godoc/pickers/native.lua @@ -2,7 +2,7 @@ local M = {} --- @param adapter GoDocAdapter --- @param config GoDocConfig ---- @param callback fun(choice: string|nil) +--- @param callback fun(choice: GoDocCallbackData) function M.show(adapter, config, callback) -- Create picker configuration local opts = { @@ -16,7 +16,12 @@ function M.show(adapter, config, callback) opts = vim.tbl_deep_extend("force", opts, config.picker.native) end - vim.ui.select(adapter.get_items(), opts, callback) + vim.ui.select(adapter.get_items(), opts, function(choice) + callback({ type = "show_documentation", choice = choice }) + end) end +-- NOTE: goto definition is not supported in native picker +M.goto_definition = nil + return M diff --git a/lua/godoc/pickers/snacks.lua b/lua/godoc/pickers/snacks.lua index 54e0105..9580312 100644 --- a/lua/godoc/pickers/snacks.lua +++ b/lua/godoc/pickers/snacks.lua @@ -3,7 +3,7 @@ local M = {} --- @param adapter GoDocAdapter --- @param config GoDocConfig ---- @param callback fun(choice: string|nil) +--- @param callback fun(data: GoDocCallbackData) function M.show(adapter, config, callback) local snacks = require("snacks") @@ -35,11 +35,27 @@ function M.show(adapter, config, callback) ctx.preview:reset() end end or nil, + win = { + input = { + keys = { + ["gd"] = { + "goto_definition", + mode = { "n" }, + }, + }, + }, + }, actions = { confirm = function(picker, item) if item then snacks.picker.actions.close(picker) - callback(item.item_name) + callback({ type = "show_documentation", choice = item.item_name }) + end + end, + goto_definition = function(picker, item) + if item then + snacks.picker.actions.close(picker) + callback({ type = "goto_definition", choice = item.item_name }) end end, }, @@ -53,4 +69,9 @@ function M.show(adapter, config, callback) snacks.picker.pick(opts) end +--- @return nil +function M.goto_definition() + require("snacks").picker.lsp_definitions() +end + return M diff --git a/lua/godoc/pickers/telescope.lua b/lua/godoc/pickers/telescope.lua index 6bc38f1..937badf 100644 --- a/lua/godoc/pickers/telescope.lua +++ b/lua/godoc/pickers/telescope.lua @@ -3,7 +3,7 @@ local M = {} --- @param adapter GoDocAdapter --- @param config GoDocConfig ---- @param callback fun(choice: string|nil) +--- @param callback fun(data: GoDocCallbackData) function M.show(adapter, config, callback) local action_state = require("telescope.actions.state") local finders = require("telescope.finders") @@ -28,7 +28,14 @@ function M.show(adapter, config, callback) local function on_package_select(prompt_bufnr) local selection = action_state.get_selected_entry() if selection then - callback(selection.value) + callback({ type = "show_documentation", choice = selection.value }) + end + end + + local function on_goto_definition(prompt_bufnr) + local selection = action_state.get_selected_entry() + if selection then + callback({ type = "goto_definition", choice = selection.value }) end end @@ -65,6 +72,9 @@ function M.show(adapter, config, callback) map("n", "", function(prompt_bufnr) on_package_select(prompt_bufnr) end) + map("n", "gd", function(prompt_bufnr) + on_goto_definition(prompt_bufnr) + end) return true end, } @@ -77,4 +87,9 @@ function M.show(adapter, config, callback) pickers.new(opts, {}):find() end +--- @return nil +function M.goto_definition() + require("telescope.builtin").lsp_definitions() +end + return M diff --git a/lua/godoc/types.lua b/lua/godoc/types.lua index f5ae66f..64cfcf5 100644 --- a/lua/godoc/types.lua +++ b/lua/godoc/types.lua @@ -8,6 +8,7 @@ --- @field get_items fun(): string[] Function that returns a list of available items --- @field get_content fun(choice: string): string[] Function that returns the content --- @field get_syntax_info fun(): GoDocSyntaxInfo Function that returns syntax info +--- @field goto_definition? fun(choice: string, picker_gotodef_fun: fun()?): nil Function that returns the definition location --- @field health? fun(): GoDocHealthCheck[] Optional health check function --- @class GoDocAdapterOpts @@ -15,6 +16,7 @@ --- @field get_items? fun(): string[] Override the get_items function --- @field get_content? fun(choice: string): string[] Override the get_content function --- @field get_syntax_info? fun(): GoDocSyntaxInfo Override the get_syntax_info function +--- @field goto_definition? fun(choice: string, picker_gotodef_fun: fun()?): nil Override the get_definition function --- @field health? fun(): GoDocHealthCheck[] Override the health check function --- @field [string] any Other adapter-specific options @@ -43,9 +45,14 @@ --- @class GoDocWindowConfig --- @field type "split"|"vsplit" The type of window to open ---- @class GoDocPicker ---- @field show fun(adapter: GoDocAdapter, user_config: GoDocConfig, callback: fun(choice: string|nil)) Shows the picker UI with items from adapter +--- @class GoDocCallbackData +--- @field type "show_documentation" | "goto_definition" +--- @field choice string? +--- @class GoDocPicker +--- @field show fun(adapter: GoDocAdapter, user_config: GoDocConfig, callback: fun(data:GoDocCallbackData)) Shows the picker UI with items from adapter +--- @field goto_definition fun()? Picker-specific implementation of "go to definition" +--- --- @class GoDocPickerConfig --- @field type "native"|"telescope"|"snacks"|"mini"|"fzf_lua" The type of picker to use --- @field native? table Options for native picker