diff --git a/.stylua.toml b/.stylua.toml new file mode 100644 index 0000000..70b4478 --- /dev/null +++ b/.stylua.toml @@ -0,0 +1,4 @@ +column_width = 80 +indent_type = "Spaces" +indent_width = 2 +quote_style = "ForceSingle" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dd5b3a5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,174 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/README.md b/README.md new file mode 100644 index 0000000..171b3bd --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# inlay-hints.nvim diff --git a/lua/inlay-hints.lua b/lua/inlay-hints.lua new file mode 100644 index 0000000..35a5b90 --- /dev/null +++ b/lua/inlay-hints.lua @@ -0,0 +1,140 @@ +local M = {} + +local lsp = require('inlay-hints.lsp') + +local default_options = { + nerdfonts = true, + render = { + type_symbol = '‣ ', + return_symbol = ' ', + variable_separator = ': ', + type_separator = ', ', + return_separator = ', ', + type_return_separator = ' ', + highlight = 'Comment', + }, +} + +local no_nerdfonts_options = { + render = { + type_symbol = '> ', + return_symbol = '< ', + }, +} + +local options = nil + +local namespace = vim.api.nvim_create_namespace('inlay-hints') + +local function setup_autocmd(events, bufnr, server_name) + vim.api.nvim_command( + string.format( + 'autocmd %s %s :lua require"inlay-hints.lsp".set_inlay_hints(%s,%s)', + events, + bufnr and ('') or '*', + bufnr or 0, + vim.inspect(server_name) + ) + ) +end + +function M.options() + return vim.tbl_deep_extend('force', {}, options) +end + +function M.setup(opts) + opts = opts or {} + if opts.nerdfonts == nil then + opts.nerdfonts = true + end + + if not opts.nerdfonts then + opts = vim.tbl_deep_extend('keep', opts, no_nerdfonts_options) + end + + options = vim.tbl_deep_extend('keep', opts, default_options) +end + +function M.setup_autocmd(bufnr, server_name) + local events = 'BufEnter,BufWinEnter,TabEnter,BufWritePost' + setup_autocmd(events, bufnr, server_name) +end + +function M.on_attach(server, bufnr) + return require('inlay-hints.lsp').on_attach(server, bufnr) +end + +function M.clear_inlay_hints(bufnr) + vim.api.nvim_buf_clear_namespace(bufnr or 0, namespace, 0, -1) +end + +M.lsp_options = lsp.lsp_options +function M.lsp_setup(name, opts) + local nvim_lsp = require('lspconfig') + + nvim_lsp[name].setup(M.lsp_options(opts)) +end + +local function default_render(bufnr, hints, set_extmark) + local lines = {} + + for _, varhint in ipairs(hints.variables) do + local line = varhint.range['end'].line + 1 + lines[line] = lines[line] or { types = '', returns = '' } + if string.len(lines[line].types) > 0 then + lines[line].types = lines[line].types .. options.render.type_separator + end + lines[line].types = lines[line].types + .. varhint.name + .. options.render.variable_separator + .. varhint.type + end + + for _, rethint in ipairs(hints.returns) do + local line = rethint.range['end'].line + 1 + lines[line] = lines[line] or { types = '', returns = '' } + if string.len(lines[line].returns) > 0 then + lines[line].returns = lines[line].returns + .. options.render.return_separator + end + lines[line].returns = lines[line].returns .. rethint.type + end + + for line, hint in pairs(lines) do + local text = '' + + if type(hint.types) == 'string' and string.len(hint.types) > 0 then + text = options.render.type_symbol .. hint.types + end + + if type(hint.returns) == 'string' and string.len(hint.returns) > 0 then + if string.len(text) > 0 then + text = text .. options.render.type_return_separator + end + text = text .. options.render.return_symbol .. hint.returns + end + + if string.len(text) > 0 then + set_extmark(line - 1, 0, { + virt_text_pos = 'eol', + virt_text = { { text, options.render.highlight } }, + hl_mode = 'combine', + }) + end + end +end + +function M.render(bufnr, hints) + M.clear_inlay_hints(bufnr) + local function set_extmark(...) + return vim.api.nvim_buf_set_extmark(bufnr, namespace, ...) + end + + (type(options.render) == 'function' and options.render or default_render)( + bufnr, + hints, + set_extmark + ) +end + +return M diff --git a/lua/inlay-hints/lsp-installer.lua b/lua/inlay-hints/lsp-installer.lua new file mode 100644 index 0000000..143d370 --- /dev/null +++ b/lua/inlay-hints/lsp-installer.lua @@ -0,0 +1,12 @@ +local M = {} + +function M.setup(server, opts) + opts = require('inlay-hints').lsp_options(server.name, opts) + return server:setup(opts) +end + +function M.on_server_ready(server) + return M.setup(server) +end + +return M diff --git a/lua/inlay-hints/lsp/init.lua b/lua/inlay-hints/lsp/init.lua new file mode 100644 index 0000000..114a87a --- /dev/null +++ b/lua/inlay-hints/lsp/init.lua @@ -0,0 +1,101 @@ +local M = {} + +local utils = require('inlay-hints.utils') + +local server_names = { + rust_analyzer = 'rust_analyzer', +} + +local servers = {} + +local function create_cache(name) + if not server_names[name] then + return + end + + local server = require('inlay-hints.lsp.' .. server_names[name]) + + if not server.lsp_handlers then + server.lsp_handlers = function() + return {} + end + end + + if not server.lsp_options then + server.lsp_options = function() + return {} + end + end + + if not server.set_inlay_hints then + server.set_inlay_hints = function() end + end + + if not server.on_attach then + function server.on_attach(s, bufnr) + if s.name ~= name then + return + end + + require('inlay-hints').setup_autocmd(bufnr, s.name) + server.set_inlay_hints(bufnr) + end + end + + server._lsp_handlers = server.lsp_handlers + function server.lsp_handlers(opts) + return utils.deep_extend('force', opts or {}, server._lsp_handlers()) + end + + server._lsp_options = server.lsp_options + function server.lsp_options(opts) + opts = utils.deep_extend('force', opts or {}, server._lsp_options()) + opts.handlers = server.lsp_handlers(opts.handlers) + if type(opts.on_attach) == 'function' then + opts.on_attach = utils.concat_functions(opts.on_attach, server.on_attach) + else + opts.on_attach = server.on_attach + end + + return opts + end + + servers[name] = server +end + +function M.get(name) + create_cache(name) + return servers[name] +end + +function M.on_attach(s, bufnr) + local server = M.get(s.name) + if server and server.on_attach then + return server.on_attach(s, bufnr) + end +end + +function M.lsp_handlers(name, opts) + local server = M.get(name) + if server then + return server.lsp_handlers(opts) + end + return opts +end + +function M.lsp_options(name, opts) + local server = M.get(name) + if server then + return server.lsp_options(opts) + end + return opts +end + +function M.set_inlay_hints(bufnr, name) + local server = M.get_server(name) + if server then + server.set_inlay_hints(bufnr) + end +end + +return M diff --git a/lua/inlay-hints/lsp/rust_analyzer.lua b/lua/inlay-hints/lsp/rust_analyzer.lua new file mode 100644 index 0000000..54ff5d3 --- /dev/null +++ b/lua/inlay-hints/lsp/rust_analyzer.lua @@ -0,0 +1,94 @@ +local M = {} + +local utils = require('inlay-hints.utils') + +-- [1]: error +-- [2]: v +-- kind: TypeHint, label: type, range +-- kind: ParameterHint, label: name, range +-- kind: ChainingHint, label: type, range +-- [3]: {bufnr, client_id, method, params={textDocument={uri}}} +-- [4]: config +local function handler(error, hints, info, _, filter) + if error then + return + end + + if vim.api.nvim_get_current_buf() ~= info.bufnr then + return + end + + local new_hints = { variables = {}, returns = {} } + + for _, hint in ipairs(hints) do + if hint.kind == 'TypeHint' then + if filter(hint.range) then + table.insert(new_hints.variables, { + type = hint.label, + name = utils.get_text(info.bufnr, hint.range), + range = hint.range, + }) + end + elseif hint.kind == 'ChainingHint' then + if filter(hint.range) then + table.insert(new_hints.returns, { + type = hint.label, + range = hint.range, + }) + end + end + end + + require('inlay-hints').render(info.bufnr, new_hints) +end + +local function filtered_handler(filter) + return function(...) + handler( + select(1, ...), + select(2, ...), + select(3, ...), + select(4, ...), + filter + ) + end +end + +local function on_server_start(_, result) + if result.quiescent then + require('inlay-hints').on_attach( + { name = 'rust_analyzer' }, + vim.api.nvim_get_current_buf() + ) + end +end + +function M.lsp_handlers() + -- compatibility with rust-tools + local ok, rust_tools = pcall(require, 'rust-tools.server_status') + local server_status = on_server_start + if ok then + server_status = utils.concat_functions(rust_tools.handler, server_status) + end + + return { + ['experimental/serverStatus'] = utils.make_handler(server_status), + } +end + +function M.lsp_options() + return { handlers = M.lsp_handlers() } +end + +function M.set_inlay_hints(bufnr, filter) + utils.request( + bufnr, + 'rust-analyzer/inlayHints', + utils.get_params(), + filtered_handler(filter or function() + return true + end) + ) +end + +return M diff --git a/lua/inlay-hints/lspconfig.lua b/lua/inlay-hints/lspconfig.lua new file mode 100644 index 0000000..3cd43f6 --- /dev/null +++ b/lua/inlay-hints/lspconfig.lua @@ -0,0 +1,11 @@ +local M = {} + +function M.setup(name, opts) + return require('inlay-hints').lsp_setup(name, opts) +end + +function M.options(name, opts) + return require('inlay-hints').lsp_options(name, opts) +end + +return M diff --git a/lua/inlay-hints/rust-tools.lua b/lua/inlay-hints/rust-tools.lua new file mode 100644 index 0000000..7ab20a4 --- /dev/null +++ b/lua/inlay-hints/rust-tools.lua @@ -0,0 +1,8 @@ +local M = {} + +function M.setup(opts) + opts.server = require('inlay-hints').lsp_options('rust_analyzer', opts.server) + return require('rust-tools').setup(opts) +end + +return M diff --git a/lua/inlay-hints/utils.lua b/lua/inlay-hints/utils.lua new file mode 100644 index 0000000..7913f92 --- /dev/null +++ b/lua/inlay-hints/utils.lua @@ -0,0 +1,99 @@ +local M = {} + +function M.get_params() + return { textDocument = vim.lsp.util.make_text_document_params() } +end + +function M.make_handler(fn) + return function(...) + local config_or_client_id = select(4, ...) + if type(config_or_client_id) ~= 'number' then + fn(...) + else + local err = select(1, ...) + local method = select(2, ...) + local result = select(3, ...) + local client_id = select(4, ...) + local bufnr = select(5, ...) + local config = select(6, ...) + fn( + err, + result, + { method = method, client_id = client_id, bufnr = bufnr }, + config + ) + end + end +end + +function M.request(bufnr, method, params, handler) + return vim.lsp.buf_request(bufnr, method, params, M.make_handler(handler)) +end + +function M.get_text(bufnr, range) + local lines = vim.api.nvim_buf_get_lines( + bufnr, + range.start.line, + range['end'].line + 1, + false + ) + + if #lines == 0 then + return lines + end + + lines[1] = string.sub(lines[1], range.start.character + 1) + lines[#lines] = string.sub( + lines[#lines], + 1, + range['end'].character - range.start.character + ) + + return table.concat(lines, '\n') +end + +function M.concat_functions(a, b) + return function(...) + a(...) + return b(...) + end +end + +function M.deep_extend(policy, ...) + local result = {} + + local function helper(policy, k, a, b) + if type(a) == 'function' and type(b) == 'function' then + return M.concat_functions(a, b) + elseif type(a) == 'table' and type(b) == 'table' then + return M.deep_extend(policy, a, b) + else + if policy == 'error' then + error( + 'Key ' + .. vim.inspect(k) + .. ' is already present with value ' + .. vim.inspect(b) + ) + elseif policy == 'force' then + return b + else + return a + end + end + end + + for _, t in ipairs({ ... }) do + for k, v in pairs(t) do + if result[k] ~= nil then + result[k] = helper(policy, k, result[k], v) + else + result[k] = v + end + end + end + + return result +end + +return M