--part of code was from rust-tools inlay_hints.lua -- I was so jealous of rust-tools which provides inlay_hints until today gopls provides this feature local M = {} local vim = vim local api = vim.api local fn = vim.fn local utils = require('go.utils') local log = utils.log local trace = utils.trace local config -- Update inlay hints when opening a new buffer and when writing a buffer to a -- file -- opts is a string representation of the table of options local should_update = {} function M.setup() local events = { 'BufWritePost', 'BufEnter', 'InsertLeave', 'FocusGained', 'CursorHold'} config = _GO_NVIM_CFG.lsp_inlay_hints if config.only_current_line then local user_events = vim.split(config.only_current_line_autocmd, ',') events = vim.tbl_extend('keep', events, user_events) end local cmd_group = api.nvim_create_augroup('gopls_inlay', {}) api.nvim_create_autocmd(events, { group = cmd_group, pattern = { '*.go', '*.mod' }, callback = function() if not vim.wo.diff then require('go.inlay').set_inlay_hints() end end, }) api.nvim_create_user_command('GoToggleInlay', function(_) require('go.inlay').toggle_inlay_hints() end, { desc = 'toggle gopls inlay hints' }) vim.defer_fn(function() require('go.inlay').set_inlay_hints() end, 1000) end local function get_params() local start_pos = api.nvim_buf_get_mark(0, '<') local end_pos = api.nvim_buf_get_mark(0, '>') local params = { range = { start = { character = 0, line = 0 }, ['end'] = { character = 0, line = 0 } } } local len = vim.api.nvim_buf_line_count(0) if end_pos[1] <= len then params = vim.lsp.util.make_given_range_params() end params['range']['start']['line'] = 0 params['range']['end']['line'] = vim.api.nvim_buf_line_count(0) - 1 trace(params) return params end local namespace = vim.api.nvim_create_namespace('experimental/inlayHints') -- whether the hints are enabled or not local enabled = nil -- parses the result into a easily parsable format -- input -- kind=1: return ; kind = 2: param -- { { -- kind = 1, -- label = { { -- value = "[]int" -- } }, -- paddingLeft = true, -- position = { -- character = 7, -- line = 8 -- } -- }, { -- kind = 2, -- label = { { -- value = "stack:" -- } }, -- paddingRight = true, -- position = { -- character = 29, -- line = 8 -- } -- }, -- example: -- { -- ["12"] = { { -- kind = "TypeHint", -- label = "String" -- } }, -- } local function parseHints(result) trace(result) local map = {} local only_current_line = config.only_current_line if type(result) ~= 'table' then return {} end for _, value in pairs(result) do local range = value.position local line = value.position.line local label = value.label local kind = value.kind local current_line = vim.api.nvim_win_get_cursor(0)[1] local function add_line() if map[line] ~= nil then table.insert(map[line], { label = label, kind = kind, range = range }) else map[line] = { { label = label, kind = kind, range = range } } end end if only_current_line then if line == current_line - 1 then add_line() end else add_line() end end return map end local function get_max_len(bufnr, parsed_data) local max_len = -1 for key, _ in pairs(parsed_data) do local line = tonumber(key) local current_line = vim.api.nvim_buf_get_lines(bufnr, line, line + 1, false)[1] if current_line then local current_line_len = string.len(current_line) max_len = math.max(max_len, current_line_len) end end return max_len end function nvim10_inline_hints(bufnr, vtext, hint, config) if hint and hint.kind == 1 then vtext = ' ' .. vtext end pcall(function() vim.api.nvim_buf_set_extmark(bufnr, namespace, hint.range.line, hint.range.character, { virt_text_pos = "inline", virt_text = { { vtext, config.highlight }, }, strict = false, hl_mode = 'combine', }) end) end local function handler_inline(err, result, ctx) trace(result, ctx) if err or result == nil then return end local bufnr = ctx.bufnr if vim.api.nvim_get_current_buf() ~= bufnr then return end local function unpack_label(label) local labels = '' for _, value in pairs(label) do labels = labels .. ' ' .. value.value end return utils.trim(labels) end -- clean it up at first M.disable_inlay_hints() local parsed = parseHints(result) trace(parsed) -- parsed is a map of line numbers to hints, -- hint includes label, range, and kind -- I only plan to deal gopls response for key, value in pairs(parsed) do trace(key, value) for _, hint in pairs(value) do trace(hint) local label = unpack_label(hint.label) trace(bufnr, namespace, label, hint, config) nvim10_inline_hints(bufnr, label, hint, config) end end end local function handler(err, result, ctx) trace(result, ctx) if err then return end local bufnr = ctx.bufnr if vim.api.nvim_get_current_buf() ~= bufnr then return end local function unpack_label(label) local labels = '' for _, value in pairs(label) do labels = labels .. ' ' .. value.value end return labels end -- clean it up at first M.disable_inlay_hints() local parsed = parseHints(result) trace(parsed) for key, value in pairs(parsed) do local virt_text = '' local line = tonumber(key) local current_line = vim.api.nvim_buf_get_lines(bufnr, line, line + 1, false)[1] if current_line then local current_line_len = string.len(current_line) local param_hints = {} local other_hints = {} -- segregate paramter hints and other hints for _, value_inner in ipairs(value) do trace(value_inner) if value_inner.kind == 2 then table.insert(param_hints, unpack_label(value_inner.label)) end if value_inner.kind == 1 then table.insert(other_hints, value_inner) end end trace(config, param_hints) -- show parameter hints inside brackets with commas and a thin arrow if not vim.tbl_isempty(param_hints) and config.show_parameter_hints then virt_text = virt_text .. config.parameter_hints_prefix .. '(' for i, value_inner_inner in ipairs(param_hints) do virt_text = virt_text .. value_inner_inner:sub(2, -2) if i ~= #param_hints then virt_text = virt_text .. ', ' end end virt_text = virt_text .. ') ' trace(virt_text) end -- show other hints with commas and a thicc arrow if not vim.tbl_isempty(other_hints) then virt_text = virt_text .. config.other_hints_prefix for i, value_inner_inner in ipairs(other_hints) do if value_inner_inner.kind == 2 and config.show_variable_name then local char_start = value_inner_inner.range.start.character local char_end = value_inner_inner.range['end'].character trace(current_line, char_start, char_end) local variable_name = string.sub(current_line, char_start + 1, char_end) virt_text = virt_text .. variable_name .. ': ' .. value_inner_inner.label else trace(value_inner_inner.label) local label = unpack_label(value_inner_inner.label) if string.sub(label, 1, 2) == ': ' then virt_text = virt_text .. label:sub(3) else virt_text = virt_text .. label end end if i ~= #other_hints then virt_text = virt_text .. ', ' end end end if config.right_align then virt_text = virt_text .. string.rep(' ', config.right_align_padding) end if config.max_len_align then local max_len = get_max_len(bufnr, parsed) virt_text = string.rep(' ', max_len - current_line_len + config.max_len_align_padding) .. virt_text end -- set the virtual text if it is not empty if virt_text ~= '' then vim.api.nvim_buf_set_extmark(bufnr, namespace, line, 0, { virt_text_pos = config.right_align and 'right_align' or 'eol', virt_text = { { virt_text, config.highlight }, }, hl_mode = 'combine', }) end -- update state enabled = true end end end function M.toggle_inlay_hints() if enabled then M.disable_inlay_hints(true) else M.set_inlay_hints() end enabled = not enabled end function M.disable_inlay_hints(update) -- clear namespace which clears the virtual text as well vim.api.nvim_buf_clear_namespace(0, namespace, 0, -1) if update then local fname = fn.expand('%:p') should_update[fname] = nil end end local found = false -- Sends the request to gopls to get the inlay hints and handle them function M.set_inlay_hints() local bufnr = vim.api.nvim_get_current_buf() -- check if lsp is ready if not found then for _, lsp in pairs(vim.lsp.get_active_clients({ bufnr = bufnr })) do if lsp.name == 'gopls' then found = true break end end end if not found then return end if vim.wo.diff then return end local fname = fn.expand('%:p') local filetime = fn.getftime(fname) if should_update[fname] == filetime then return end local h = handler local nvim10 = vim.fn.has('nvim-0.10') == 1 if nvim10 then h = handler_inline end vim.defer_fn(function() vim.lsp.buf_request(bufnr, 'textDocument/inlayHint', get_params(), h) should_update[fname] = filetime end, 100) end return M --[[ { kind = 1, label = { { value = "error" -- this is return value } }, range = { character = 8, line = 78 } } { kind = 2, label = { { value = "path:" } }, range = { character = 30, line = 78 } } ]]--