Repository: Fildo7525/pretty_hover Branch: master Commit: a4212464431e Files: 17 Total size: 54.4 KB Directory structure: gitextract_zbixqniw/ ├── .github/ │ └── ISSUE_TEMPLATE/ │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── examples/ │ └── parsing.lua └── lua/ └── pretty_hover/ ├── config.lua ├── core/ │ ├── compatibility.lua │ └── util.lua ├── highlight.lua ├── init.lua ├── local_request.lua ├── number.lua └── parser/ ├── init.lua ├── parser.lua └── references.lua ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .gitignore ================================================ lua/.luarc.json test ================================================ FILE: CONTRIBUTING.md ================================================ Anybody is welcome to contribute to this project. If you have any idea, how to improve any features or you have an idea for a new feature do not hesitate to fork this project and create a PR. Any improvement is welocme. In case you want to contribute to this project, here is the structure of the project: **/core** - *compatibility.lua* - All the functions that change during the nvim versions. - *util.lua* - Utility project functions. **/parser** - *init.lua* - Parser module public implementation. This should be used to parse the buffer. It is used internally by the plugin, too. - *parser.lua* - Internal implementation of the parser. The parser improvement or functionality has to be extended here. - *references.lua* - Parses the lines and detects the references in the buffer. **/** - *config.lua* - Default configuration of the plugin. This is used to set the default configuration of the plugin. - *highlight.lua* - Module applying the highlighting detected by the parser. - *init.lua* - The main module of the plugin. This supplys the public API of the plugin. - *number.lua* - Module that creates a popup window with the number interpratation in multiple bases. ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 Filip Lobpreis Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

pretty_hover

Stargazers Issues Contributors

## Table of contents - [How it looks](#how-it-looks) - [Installation and setup](#installation-and-setup) - [Configuration](#configuration) - [Integration](#integration) - [Blink.cmp](#blink.cmp) - [Default config](#default-configuration) - [Limitations](#limitations) - [Contributing](#contributing) - [Inspiration](#inspiration) Pretty_hover is a lightweight plugin that parses the hover message before opening the popup window. The output can be easily manipulated with. This will result in a more readable hover message. An additional feature is `number conversion`. If you are tired of constantly converting some numbers to hex, octal or binary you can use this plugin to do it for you. ### How it looks > _**NOTE**_: The colors of the text depend on the color of your chosen colorscheme. These pictures are taken with colorscheme `catppuccin-mocha` Using native vim.lsp.buf.hover() Using pretty_hover ## Installation and setup ### via Lazy ```lua { "Fildo7525/pretty_hover", event = "LspAttach", opts = {} }, ``` ### via Packer ```lua use { "Fildo7525/pretty_hover", config = function() require("pretty_hover").setup({}) end } ``` ### Using Pretty Hover To open a hover window, run the following lua snippet (or bind it to a key) ```lua require("pretty_hover").hover() ``` To close a hover window either move the cursor as with nvim's hover popup or run the following lua snippet (e.g. from a keymap) ```lua require("pretty_hover").close() ``` **NOTE: When focused on a hover window, you can also press `q` to close the hover window** ### Configuration | Parameter | Description | |----------------- | -------------- | | line | If one of the supplied strings is located as the first word in the line the whole line is surrounded by `line.styler`. | | listing | These words will be substituted with `listing.styler`. | | group | Table containing group name and its detectors. If this word is detected at the beginning of a line the next word is surrounded by `group.styler`. The whole group is separated by an line and the first line containing es the group name. | | header | List of strings. If this word is detected at the beginning of a line the word is substituted by `header.styler` | | return statement | This words are substituted with **Return** (in bold) | | references | If any word from this list is detected, the next word is surrounded by `references.styler[1]`. If this word is located in `line` section the next word is surrounded by `references.styler[2]` (see [Limitations](#limitations)) | | hl | This is a table of highlighting groups. You can define new groups by specifying at least two parameters. `color` and `detect`. Flag `line` is not mandatory, however by setting this flag you can ensure that the whole line is highlighted. When a detector from the table `detect` is found the detector is made uppercase, omits the beginning tag and gets highlighted. | | border | Sets the border of the hover window. (none \| single \| double \| rounded \| solid \| shadow). | | wrap | Flag whether to wrap the text if the window is smaller. Otherwise the floating window is scrollable horizontally | | max_width | Sets the maximum width of the window. If you don't want any limitation set to nil. | | max_height | Sets the maximum height of the window. If you don't want any limitation set to nil. | | toggle | Flag detecting whether you want to have the hover just as a toggle window or make the popup focusable. | | multi_server | Flag detecting whether you want to use the new multi lsp support or not. | > _**NOTE**_: To really use this plugin you have to create a keymap that calls `require('pretty_hover').hover()` function. The plugin supports code blocks. By specifying `@code{cpp}` the text in the popup window is highlighted with its filetype highlighter until the `@endcode` is hit. When the filetype is not specified in the flag `@code` the filetype from the currently opened file is used. #### Default configuration ```lua { -- Tables grouping the detected strings and using the markdown highlighters. header = { detect = { "[\\@]class" }, styler = '###', }, line = { detect = { "[\\@]brief" }, styler = '**', }, listing = { detect = { "[\\@]li" }, styler = " - ", }, references = { detect = { "[\\@]ref", "[\\@]c", "[\\@]name" }, styler = { "**", "`" }, }, group = { detect = { -- ["Group name"] = {"detectors"} ["Parameters"] = { "[\\@]param", "[\\@]*param*" }, ["Types"] = { "[\\@]tparam" }, ["See"] = { "[\\@]see" }, ["Return Value"] = { "[\\@]retval" }, }, styler = "`", }, -- Tables used for cleaner identification of hover segments. code = { start = { "[\\@]code" }, ending = { "[\\@]endcode" }, }, return_statement = { "[\\@]return", "[\\@]*return*", }, -- Highlight groups used in the hover method. Feel free to define your own highlight group. hl = { error = { color = "#DC2626", detect = { "[\\@]error", "[\\@]bug" }, line = false, -- Flag detecting if the whole line should be highlighted }, warning = { color = "#FBBF24", detect = { "[\\@]warning", "[\\@]thread_safety", "[\\@]throw" }, line = false, }, info = { color = "#2563EB", detect = { "[\\@]remark", "[\\@]note", "[\\@]notes" }, }, -- Here you can set up your highlight groups. }, -- If you use nvim 0.11.0 or higher you can choose, whether you want to use the new -- multi lsp support or not. Otherwise this option is ignored. multi_server = true, border = "rounded", wrap = true, max_width = nil, max_height = nil, toggle = false, } ``` ### Integration The plugin supports an easy integration: ```lua local parsed = require("pretty_hover.parser").parse(text) ``` the parsed variable contains two fields `text` and `highlight`. The `text` field contains the converted text to markdown and the `highlight` field contains the highlight groups for the text. You can use the `parsed` variable to display the hover message in your own way. ```lua vim.lsp.util.open_floating_preview(parsed.text, "markdown", { focus = true, focusable = true, wrap = true, wrap_at = 100, max_width = 100, border = "rounded", focus_id = "pretty-hover-example", }) ``` To see an example of the implementation see the `pretty_hover/examples/parsing.lua` file. #### Blink.cmp This functionality is supported for blink.cmp from version v0.13.0 and higher. To use this plugin with `blink.cmp` documentation you can add the following code snippet to you configuration: ```lua { completion = { documentation = { draw = function(opts) if opts.item and opts.item.documentation and opts.item.documentation.value then local out = require("pretty_hover.parser").parse(opts.item.documentation.value) opts.item.documentation.value = out:string() end opts.default_implementation(opts) end, } }, } ``` ### Limitations Currently, Neovim supports these markdown stylers: \`, \*, \`\`\`[language]. Unfortunately, you cannot do any of their combination. If the support is extended there will be more options to style the pop-up window. Newly this plugin started supporting highlighting see the [Configuration](#configuration) for more information. ### Contributing If you have any idea how to improve this plugin do not hesitate to create a PR. Otherwise, if you know how to improve the plugin mention it in a new issue. Enjoy the plugin. ### Inspiration https://github.com/lewis6991/hover.nvim ================================================ FILE: examples/parsing.lua ================================================ local text = [[### function `main` --- → `int` Parameters: - `int argc` - `char ** argv` @brief Neque porro quisquam est qui dolorem @c ipsum quia dolor sit amet, consectetur, adipisci velit..." Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the @c industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. @note This is a note. @param argc Number of arguments from the command line. @param argv The arguments in format of the strings. @return int The return of the program. Usually 0 for successful run. --- ```cpp int main(int argc, char *argv[]) ``` ]] local text_table = { " ### function `main`", "---", " → `int`", "Parameters:", " - `int argc`", " - `char ** argv`", "", "@brief Neque porro quisquam est qui dolorem @c ipsum quia dolor sit amet, consectetur, adipisci velit...", "", "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", "Lorem Ipsum has been the @c industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.", "It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.", "It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.", "", "@note This is a note.", "", "@param argc Number of arguments from the command line.", "@param argv The arguments in format of the strings.", "@return int The return of the program. Usually 0 for successful run.", "---", "```cpp", "int main(int argc, char *argv[])", "```", } local parser = require("pretty_hover.parser") local out = parser.parse(text) --[[ local out = parser.parse(text_table) ]] local bufnr, winnr = vim.lsp.util.open_floating_preview(out.text, "markdown", { focus = true, focusable = true, wrap = true, wrap_at = 100, max_width = 100, border = "rounded", focus_id = "pretty-hover-example", }) --[[ require('pretty_hover.highlight').apply_highlight(out.highlighting, bufnr, require("pretty_hover").get_config()) ]] require('pretty_hover.highlight').apply_highlight(out.highlighting, bufnr) ================================================ FILE: lua/pretty_hover/config.lua ================================================ ---@class PrettyHoverConfig local M = { _config = {}, header = { detect = {"[\\@]class"}, styler = '###', }, line = { detect = { "[\\@]brief" }, styler = '**', }, listing = { detect = { "[\\@]li" }, styler = " - ", }, references = { detect = { "[\\@]ref", "[\\@]c", "[\\@]name", "[\\@]a", }, styler = { "**", "`" }, }, group = { detect = { ["Parameters"] = { "[\\@]param", "[\\@]*param*" }, ["Types"] = { "[\\@]tparam" }, ["See"] = { "[\\@]see" }, ["Return Value"] = { "[\\@]retval" }, }, styler = "`", }, code = { start = {"[\\@]code"}, ending = {"[\\@]endcode"}, }, return_statement = { "[\\@]return", "[\\@]*return*", }, hl = { error = { color = "#DC2626", detect = {"[\\@]error", "[\\@]bug"}, line = false, }, warning = { color = "#FBBF24", detect = {"[\\@]warning", "[\\@]thread_safety", "[\\@]throw"}, line = false, }, info = { color = "#4FC1FF", detect = {"[\\@]remark", "[\\@]note", "[\\@]notes"}, } }, multi_server = true, border = "rounded", wrap = true, max_width = nil, max_height = nil, toggle = false, } ---@class PrettyHoverConfig ---@brief This class is used to configure the pretty_hover plugin. ---@param config table Table of options to be used for the pretty_hover configuration. If none or empty is provided, the ---previous configuration will be used. If the previous configuration is also empty, the default configuration will be used. ---@return table config The configuration table that will be used for the pretty_hover plugin. function M:instance(config) config = config or {} if vim.tbl_isempty(config) and not vim.tbl_isempty(self._config) then return self._config end self._config = vim.tbl_deep_extend('force', {}, self, config) require("pretty_hover.highlight").setup_colors(self._config) return self._config end -- return M return M ================================================ FILE: lua/pretty_hover/core/compatibility.lua ================================================ local M = {} --- Function that encapsulates the changes in nvim api for getting the active clients. --- --- @return table List of active clients. function M.get_clients() if vim.version().minor >= 11 then return vim.lsp.get_clients() else return vim.lsp.get_active_clients() end end function M.nvim_hl(name, fg) if vim.version().minor >= 11 then local ns = vim.api.nvim_get_namespaces()["pretty_hover_ns"] return vim.api.nvim_get_hl(ns, {name = name}) end return vim.api.nvim_get_hl_by_name(name, fg) end return M ================================================ FILE: lua/pretty_hover/core/util.lua ================================================ local api = vim.api local hl = require("pretty_hover.highlight") local hover_ns = api.nvim_create_namespace('pretty_hover_range') local compatibility = require("pretty_hover.core.compatibility") local M = {} local winnr = 0 local bufnr = 0 function string:split(delimiter) local result = { } local from = 1 local delim_from, delim_to = string.find( self, delimiter, from ) while delim_from do table.insert( result, string.sub( self, from , delim_from-1 ) ) from = delim_to + 1 delim_from, delim_to = string.find( self, delimiter, from ) end table.insert( result, string.sub( self, from ) ) return result end --- Check if a table contains desired element. vim.tbl_contains does not work for all cases. ---@param tbl table Table to be checked. ---@param el string Element to be checked. ---@return boolean True if the table contains the element, false otherwise. function M.tbl_contains(tbl, el) if not el then return false end if not tbl then return false end for _, v in pairs(tbl) do if el:find(v) then return true end end return false end --- Checks the table for the desired element. If the element is found, it is returned, otherwise nil is returned. ---@param tbl table Table to be checked. ---@param el string Element to be checked for. ---@return string The element if it is found, nil otherwise. function M.find(tbl, el) if not el or not tbl then return "" end for _, v in pairs(tbl) do if el:find(v) then return el end end return "" end --- Count the printable strings in the table. ---@param tbl table Table of string from hover. ---@return number Number of printable lines. function M.printable_table_size(tbl) local count = 0 for _, el in pairs(tbl) do if el then count = count + 1 end end return count end --- Splits a string into a table of strings. ---@param toSplit string String to be split. ---@param separator string|nil The separator. If not defined, the separator is set to "%S+". ---@return table Table of strings split by the separator. function M.split(toSplit, separator) local indentation = nil if separator == nil then indentation = string.match(toSplit, "^%s+") separator = "%S+" end if toSplit == nil then return {} end local chunks = {} if indentation ~= nil and indentation:len() > 0 then table.insert(chunks, indentation) end for substring in toSplit:gmatch(separator) do -- These both cases are here because of python server. Some servers have '.... ' in front of every line and some -- servers surround the whole message with '```text' and '```'. This is a workaround for that. if substring:sub(1, 2) == ". " then substring = substring:sub(5) end table.insert(chunks, substring) end return chunks end --- Join the elements of a table into a string with a delimiter. ---@param tbl table Table to be joined. ---@param delim string Delimiter to be used. ---@return string Joined string. function M.join_table(tbl, delim) local result = "" for idx, chunk in pairs(tbl) do result = result .. chunk if idx ~= #tbl then result = result .. delim end end return result end --- This function checks all the active clients for current buffer and returns the active client that supports the current file type. ---@return table|nil Active client for the current buffer or nil if there is no active client. function M.get_current_active_client() for _, client in ipairs(compatibility.get_clients()) do if M.tbl_contains(client.config.filetypes, vim.bo.filetype) then return client end end return nil end --- Close the opened floating window. function M.close_float() -- Safeguard around accidentally calling close when there is no pretty_hover window open if winnr == 0 and bufnr == 0 then return end api.nvim_buf_clear_namespace(vim.fn.bufnr(), hover_ns, 0, -1) -- Before closing the window, check if it is still valid. if not api.nvim_win_is_valid(winnr) then winnr = 0 bufnr = 0 return end api.nvim_win_close(winnr, true) winnr = 0 bufnr = 0 end --- The file is a link in markdown style and is represented as [\w+](#L,). --- This function opens the file in a new buffer and jumps to the given line and column. function M.open_file_under_cursor() local line = api.nvim_get_current_line() local target = line:match("%[(.-)%]%((.-)#L(%d+),?(%d*)%)") if not target then vim.notify("1. No valid file link under cursor", vim.log.levels.WARN) return end local _, uri, row, col = line:match("%[(.-)%]%((.-)#L(%d+),?(%d*)%)") if not uri or not row then vim.notify("2. No valid file link under cursor", vim.log.levels.WARN) return end row = tonumber(row) col = tonumber(col) or 0 M.close_float() -- Open the file in a new buffer vim.cmd("edit " .. uri) -- Jump to the specified line and column api.nvim_win_set_cursor(0, {row, col}) end --- Opens a floating window with the documentation transformed from doxygen to markdown. ---@param hover_text string[] Text to be converted. ---@param format string Filetype to be used for the conversion. ---@param config table Table of options to be used for the conversion to the markdown language. function M.open_float(hover_text, format, config) if not hover_text or #hover_text == 0 then -- There is nothing to display, quit out early local tabled_numbers = require("pretty_hover.number").get_number_representations() if not tabled_numbers then vim.notify("No information available", vim.log.levels.INFO) return end M.open_float(tabled_numbers:split("\n"), format, config) return end -- Convert Doxygen comments to Markdown format local out = require("pretty_hover.parser").parse(hover_text) if #out.text == 0 then vim.notify("No information available", vim.log.levels.INFO) return end if config.toggle and winnr ~= 0 then M.close_float() return end local language = format if config.one_liner then language = vim.bo.filetype end bufnr, winnr = vim.lsp.util.open_floating_preview(out.text, language, { border = config.border, focusable = true, focus = true, focus_id = "pretty-hover", wrap = config.wrap, wrap_at = config.max_width and config.max_width - 2 or nil, max_width = config.max_width, max_height = config.max_height, }) vim.wo[winnr].foldenable = false vim.bo[bufnr].modifiable = false vim.bo[bufnr].bufhidden = 'wipe' hl.apply_highlight(out.highlighting, bufnr, config) vim.keymap.set('n', 'gf', M.open_file_under_cursor, { buffer = bufnr, silent = true, nowait = true, }) vim.keymap.set('n', 'q', M.close_float, { buffer = bufnr, silent = true, nowait = true, }) return bufnr, winnr end return M ================================================ FILE: lua/pretty_hover/highlight.lua ================================================ local M = {} local compatibility = require "pretty_hover.core.compatibility" local api = vim.api --- Convert HEX color representation to RGB ---@param hex string HEX color representation ---@return number|nil, number|nil, number|nil # RGB color representation function M.hex2rgb(hex) hex = hex:gsub("#", "") return tonumber("0x" .. hex:sub(1, 2)), tonumber("0x" .. hex:sub(3, 4)), tonumber("0x" .. hex:sub(5, 6)) end --- Check if HEX color is dark ---@param hex string HEX color representation ---@return boolean True if color is dark, false otherwise function M.is_dark(hex) local r, g, b = M.hex2rgb(hex) local lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255 return lum <= 0.5 end --- Get the highlight group ---@param name string Highlight group name ---@return table|nil Highlight group function M.get_hl(name) local hl = compatibility.nvim_hl(name, true) for _, key in pairs({ "foreground", "background", "special" }) do if hl[key] then hl[key] = string.format("#%06x", hl[key]) end end return hl end --- Setup color groups for pretty_hover plugin. ---@param config table Options from the config. function M.setup_colors(config) M.hl_ns = api.nvim_create_namespace("pretty_hover_ns") local normal = M.get_hl("Normal") if not normal then vim.notify("No normal highlight group found", vim.log.levels.WARN) return end for kw, hl_groups in pairs(config.hl) do local kw_color = hl_groups.color or "default" local hex if kw_color:sub(1, 1) == "#" then hex = kw_color else local colors = M.options.colors[kw_color] colors = type(colors) == "string" and { colors } or colors for _, color in pairs(colors) do if color:sub(1, 1) == "#" then hex = color break end local c = M.get_hl(color) if c and c.foreground then hex = c.foreground break end end end if not hex then error("Todo: no color for " .. kw) end vim.cmd("hi def PH" .. kw .. " guibg=NONE guifg=" .. hex .. " gui=NONE") end end --- Applies the highlight to the lines of the opened floating window. --- The used groups are ErrorMsg and WarningMsg. For the proper highlighting, the --- highlight groups must be defined. ---@param hl_data table Table of control variables that were set during the conversion to markdown. ---@param bufnr number Buffer number of the pop-up window. ---@param config table|nil Table of configurations. ---@overload fun(hl_data: table, bufnr: number) ---@overload fun(hl_data: table, bufnr: number, config: table) function M.apply_highlight(hl_data, bufnr, config) if not config then config = require("pretty_hover").get_config() end if M.hl_ns then api.nvim_buf_clear_namespace(bufnr, M.hl_ns, 0, -1) end M.hl_ns = api.nvim_create_namespace("pretty_hover_ns") for name, _ in pairs(config.hl) do if hl_data.lines[tostring(name)] then for _, line in pairs(hl_data.lines[tostring(name)]) do if type(line) == "table" then api.nvim_buf_add_highlight(bufnr, M.hl_ns, "PH"..tostring(name), line.line_nr, 0, line.to); end end end end end return M ================================================ FILE: lua/pretty_hover/init.lua ================================================ local api = vim.api local cfg = require("pretty_hover.config") local h_util = require("pretty_hover.core.util") local local_hover_request = require("pretty_hover.local_request").local_hover_request local M = {} --- Parses the response from the server and displays the hover information converted to markdown. function M.hover(config) local params = vim.lsp.util.make_position_params(0, 'utf-16') -- Check if the server for this file type exists and supports hover. local client = h_util.get_current_active_client() local hover_support_present = client and client.capabilities.textDocument.hover if not client or not hover_support_present then vim.notify("There is no client for this filetype or the client does not support the hover capability.", vim.log.levels.WARN) return end config = config or {} cfg:instance().hover_cnf = config vim.lsp.buf_request_all(0, "textDocument/hover", params, local_hover_request) end --- Setup the plugin to use the given options. ---@param config table Options to be set for the plugin. function M.setup(config) config = cfg:instance(config) if config.toggle then local id = api.nvim_create_augroup("pretty_hover_augroup", { clear = true, }) api.nvim_create_autocmd({ "CursorMoved" }, { callback = function() require("pretty_hover.core.util").close_float() end, group = id, }) end end --- Close the opened floating window. function M.close() h_util.close_float() end function M.get_config() return cfg:instance() end return M ================================================ FILE: lua/pretty_hover/local_request.lua ================================================ local api = vim.api local lsp = vim.lsp local util = vim.lsp.util local hover_ns = api.nvim_create_namespace('pretty_hover_range') local cfg = require("pretty_hover.config") local h_util = require("pretty_hover.core.util") local number = require("pretty_hover.number") local M = {} local function parse_response_contents(contents) local hover_text = contents.value; -- vtsls workaround, this lsp does not contain value in the contents. It's just pure text. if type(contents) == "string" then hover_text = contents end if hover_text ~= nil then return hover_text end -- typescript-tools.nvim workaround -- Add a test in case there are no contents. if not pcall(function() hover_text = contents[1].value end) then return end hover_text = hover_text or "" for i = 2, #contents do if type(contents[i]) ~= "string" then vim.notify("Unexpected item type found in hover request's response.\n" .. "Please report an issue on github: https://github.com/Fildo7525/pretty_hover", vim.log.levels.ERROR) break end hover_text = hover_text .. contents[i] end return hover_text end local function request_below11(results) local called = false for _, response in pairs(results) do if response.result and response.result.contents and called == false then called = true local contents = response.result.contents -- We have to do this because of java. Sometimes is the value parameter split -- into two chunks. Leaving the rest of the hover message as the second argument -- in the received table. if contents.language == "java" then for _, content in pairs(contents) do local hover_text = content.value or content if not hover_text then vim.notify("There is no text to be displayed", vim.log.levels.INFO) return end h_util.open_float(hover_text, "markdown", cfg:instance()) end else local hover_text = parse_response_contents(response.result.contents) if not hover_text then vim.notify("There is no text to be displayed", vim.log.levels.INFO) return end h_util.open_float(hover_text, "markdown", cfg:instance()) end end end if not called then local hover_text = number.get_number_representations() if not hover_text then return end h_util.open_float(hover_text, "markdown", cfg:instance()) return end end local function request_above11(results, ctx) local bufnr = assert(ctx.bufnr) if api.nvim_get_current_buf() ~= bufnr then -- Ignore result since buffer changed. This happens for slow language servers. return end -- Filter errors from results local results1 = {} --- @type table for client_id, resp in pairs(results) do local err, result = resp.err, resp.result if err then lsp.log.error(err.code, err.message) elseif result then results1[client_id] = result end end if vim.tbl_isempty(results1) then if cfg:instance().hover_cnf.silent ~= true then local hover_text = number.get_number_representations() if not hover_text then vim.notify('No information available') return end h_util.open_float(hover_text, "markdown", cfg:instance()) return end return end local contents = {} --- @type string[] local nresults = #vim.tbl_keys(results1) local format = 'markdown' for client_id, result in pairs(results1) do local client = assert(lsp.get_client_by_id(client_id)) if nresults > 1 then -- Show client name if there are multiple clients contents[#contents + 1] = string.format('# %s', client.name) end if type(result.contents) == 'table' and result.contents.kind == 'plaintext' then if #results1 == 1 then format = 'plaintext' contents = vim.split(result.contents.value or '', '\n', { trimempty = true }) else -- Surround plaintext with ``` to get correct formatting contents[#contents + 1] = '```' vim.list_extend( contents, vim.split(result.contents.value or '', '\n', { trimempty = true }) ) contents[#contents + 1] = '```' end else vim.list_extend(contents, util.convert_input_to_markdown_lines(result.contents)) end local range = result.range if range then local start = range.start local end_ = range['end'] local start_idx = util._get_line_byte_from_position(bufnr, start, client.offset_encoding) local end_idx = util._get_line_byte_from_position(bufnr, end_, client.offset_encoding) vim.hl.range( bufnr, hover_ns, 'LspReferenceTarget', { start.line, start_idx }, { end_.line, end_idx }, { priority = vim.hl.priorities.user } ) end contents[#contents + 1] = '---' end -- Remove last linebreak ('---') contents[#contents] = nil if vim.tbl_isempty(contents) then if cfg:instance().hover_cnf.silent ~= true then vim.notify('No information available') end return end local _, winnr = h_util.open_float(contents, format, cfg:instance()) -- Remove selection highlighting after window is closed api.nvim_create_autocmd('WinClosed', { pattern = tostring(winnr), once = true, callback = function() api.nvim_buf_clear_namespace(bufnr, hover_ns, 0, -1) return true end, }) end --- Function that will be used in hover request invoked by lsp. ---@param results table Table of responses from the server. ---@param ctx table Context of the request. function M.local_hover_request(results, ctx) -- Multi-server support is only available in nvim-0.11 and above. -- The user can still decide to use the multi-server or not. if vim.fn.has('nvim-0.11') == 1 and cfg:instance().multi_server then request_above11(results, ctx) return end request_below11(results) end return M ================================================ FILE: lua/pretty_hover/number.lua ================================================ local M = {} --- Convert the input number to the specified base. --- @param num number Number to be converted. --- @param base number Base to convert the number to. --- @return string The number converted into specified base in a string format. function M.toBase(num, base) local baseChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" local baseStr = "" local idx = 0 while num > 0 do local rem = num % base baseStr = baseChars:sub(rem + 1, rem + 1) .. baseStr num = math.floor(num / base) idx = idx + 1 if idx % 4 == 0 and num > 0 then baseStr = " " .. baseStr end end return baseStr == "" and "0" or baseStr end --- Function to convert a number to its binary representation --- @see toBase --- @param num number Number to be converted. --- @return string The number converted into binary in a string format. function M.toBinary(num) return M.toBase(num, 2) end --- Function to convert a number to its octal representation --- @see toBase --- @param num number Number to be converted. --- @return string The number converted into octal in a string format. function M.toOctal(num) return M.toBase(num, 8) end --- Function to convert a number to its hexadecimal representation --- @see toBase --- @param num number Number to be converted. --- @return string The number converted into hexadecimal in a string format. function M.toHex(num) return M.toBase(num, 16) end --- Pretty prints the decimal number by adding spaces after every 3 digits. --- @param num string Number to be pretty printed. --- @return string The pretty printed number. function M.prettyDecimal(num) local len = #num local pretty = "" local idx = 0 for i = len, 1, -1 do pretty = num:sub(i, i) .. pretty idx = idx + 1 if idx % 3 == 0 and i ~= 1 then pretty = " " .. pretty end end return pretty end --- Get the type of the number. --- @param num string Number to get the type of. --- @return number|nil The type of the number. function M.get_number_type(num) local original if type(num) == 'string' and tonumber(num) then original = num else return nil end if original[1] ~= 0 or #original < 2 then return 10 elseif original[2] == 'x' then return 16 elseif original[2] == 'b' then return 2 elseif original[2] == 'o' then return 8 else return 10 end end --- Function to get all representations of a number --- @param num string Number to get the representations of. --- @param type number Type of the number. --- @param return_type string The type of the return value, either "string" or "table". --- @return string[]|nil A table containing the representations of the number in different bases. function M.get_numerical_representations(num, type, return_type) local tmp = tonumber(num, type) if not tmp then return nil end local decimal = M.prettyDecimal(tostring(tmp)) local binary = M.toBinary(tmp) local octal = M.toOctal(tmp) local hexadecimal = M.toHex(tmp) local s = string.format("### Number types:\n---\nBinary: 0b%s\nOctal: 0o%s\nDecimal: %s\nHexadecimal: 0x%s\n", binary, octal, decimal, hexadecimal) if return_type == "string" then return s end return s:split("\n") end --- Function to get the number representations of the current word under the cursor. --- @see get_number_type --- @param return_type? string The number to get the representations of. --- @return string|string[]|nil The number representations of the current word under the cursor. function M.get_number_representations(return_type) local num = vim.fn.expand(""); if return_type == nil then return_type = "table" end if num:sub(1,1) == '-' then num = num:sub(2) end local number_type = M.get_number_type(num) if not number_type then return end if number_type ~= 10 then num = num:sub(2) end return M.get_numerical_representations(num, number_type, return_type) end return M ================================================ FILE: lua/pretty_hover/parser/init.lua ================================================ local M = {} local parser = require("pretty_hover.parser.parser") ---@class ParserOutput ---@field public text table ---@field public highlighting table ---@field public new fun(self, text: table, highlighting: table): ParserOutput ---@field public empty fun(self): ParserOutput ---@field public string fun(self): string local ParserOutput = { text = {}, highlighting = {}, string = function(self) return table.concat(self.text, "\n") end, } --- Comparator function to check if two ParserOutput structs are equal. ---@param lhs ParserOutput ---@param rhs ParserOutput ---@return boolean Result whether the lhs and rhs sides of the comparison are the same local function ParserOutputEQ(lhs, rhs) local str_lhs = vim.inspect(lhs) local str_rhs = vim.inspect(rhs) return vim.fn.sha256(str_lhs) == vim.fn.sha256(str_rhs) end --- Creates new ParserOutput object. --- ---@param text table of parsed input. This goes directly to functions like vim.lsp.util.open_floating_preview ---@param highlighting table of highlighting data --- ---The highlighting data can be applied fx. like this --- ---```lua --- if M.hl_ns then --- api.nvim_buf_clear_namespace(bufnr, M.hl_ns, 0, -1) --- end --- --- M.hl_ns = api.nvim_create_namespace("pretty_hover_ns") --- --- for name, _ in pairs(config.hl) do --- if hl_data.lines[tostring(name)] then --- for _, line in pairs(hl_data.lines[tostring(name)]) do --- if type(line) == "table" then --- api.nvim_buf_add_highlight(bufnr, M.hl_ns, "PH"..tostring(name), line.line_nr, 0, line.to); --- end --- end --- end --- end ---``` --- ---@return ParserOutput out New object of the referenced type function ParserOutput:new(text, highlighting) local out = {} setmetatable(out, { __index = self, __eq = ParserOutputEQ }) out.text = text out.highlighting = highlighting return out end function ParserOutput:empty() return ParserOutput:new({}, {}) end --- @brief This method parses the input string or table and converts the contents from doxygen into markdown format. --- --- NOTE: The string must have new lines inside. If the string is not separated by them the parsing will not be done. --- Additionally, if nil, empty string or empty table are passed in the returned object will have empty text and highlighting --- fields. --- --- The output highlighting data can be applied fx. this --- --- ```lua --- function M.apply_highlight(config, hl_data, bufnr) ---ll if M.hl_ns then --- api.nvim_buf_clear_namespace(bufnr, M.hl_ns, 0, -1) --- end --- --- M.hl_ns = api.nvim_create_namespace("pretty_hover_ns") --- --- for name, _ in pairs(config.hl) do --- if hl_data.lines[tostring(name)] then --- for _, line in pairs(hl_data.lines[tostring(name)]) do --- if type(line) == "table" then --- api.nvim_buf_add_highlight(bufnr, M.hl_ns, "PH"..tostring(name), line.line_nr, 0, line.to); --- end --- end --- end --- end --- end --- ``` --- @param text string|table Text as a string --- @return ParserOutput Converted doxygen text into markdown. --- --- @see pretty_hover.core.util.open_float function for the implementation in this plugin --- --- @overload fun(text: string): ParserOutput --- @overload fun(text: table): ParserOutput function M.parse(text) if not text or (type(text) == "string" and text == "") or (type(text) == "table" and vim.tbl_isempty(text)) then return ParserOutput:empty() end local config = require("pretty_hover.config"):instance() local hl_data = { replacement = "", lines = {}, } local tbl = parser.convert_to_markdown(text, config, hl_data) return ParserOutput:new(tbl, hl_data) end return M ================================================ FILE: lua/pretty_hover/parser/parser.lua ================================================ local util = require("pretty_hover.core.util") local M = { brief = { detected = false, option = "", }, text_start_detected = false, } --- Transforms the line from doxygen type into markdown ---@param line string Line to be transformed. ---@param config table Table of options to be used for the conversion to the markdown language. ---@param hl_data table Table of control variables to be used for the pop-up window highlighting. ---@param control table Table of control variables to be used for the conversion to the markdown language. ---@return table Table of strings from doxygen to markdown. function M.transform_line(line, config, control, hl_data) local result = {} -- Some servers add whitespaces infornt of some rows. if line:find("^%s+[\\@]") then line = line:gsub("^%s+", "") end if line:find(" ") then line = line:gsub(" ", " ") end if line:find("^(%s*)\\%-%s*") then line = line:gsub("^(%s*)\\%-%s*", "%1- ") -- vim.print(line) end if line:find("^```text$") then M.text_start_detected = true line = "" end if line:find("^```$") and M.text_start_detected then M.text_start_detected = false line = "" end local tbl = util.split(line) local el = tbl[1] local insertEmptyLine = false for name, group in pairs(config.hl) do if util.tbl_contains(group.detect, el) then tbl[1] = string.upper(util.find(group.detect, el)) if tbl[1]:sub(1, 1) == '@' then tbl[1] = tbl[1]:sub(2) else tbl[1] = tbl[1]:sub(3) end hl_data.lines[tostring(name)].detected = true hl_data.replacement = tbl[1] end end -- Either end the brief line or extend it to the next line. if M.brief.detected and el and not el:sub(1,2):gmatch("[\\@]")() then if M.brief.option == "continue" then table.insert(result, "") M.brief.detected = false M.brief.option = "" elseif M.brief.option == "start" then tbl[1] = config.line.styler .. tbl[1] tbl[#tbl] = tbl[#tbl] .. config.line.styler M.brief.detected = false M.brief.option = "" end end if util.tbl_contains(config.header.detect, el) then tbl[1] = config.header.styler insertEmptyLine = true; elseif util.tbl_contains(config.line.detect, el) then table.remove(tbl, 1) M.brief.detected = true if #tbl == 0 then M.brief.option = "start" else tbl[1] = config.line.styler .. (tbl[1] or "") tbl[#tbl] = tbl[#tbl] .. config.line.styler M.brief.option = "continue" end elseif util.tbl_contains(config.listing.detect, el) then tbl[1] = config.listing.styler elseif util.tbl_contains(config.return_statement, el) then table.insert(result, "") tbl[1] = "**Return**" line = util.join_table(tbl, " ") elseif util.tbl_contains(config.code.start, el) then local language = el:gmatch("{(%w+)}")() or vim.o.filetype table.insert(result, "```" .. language) table.remove(tbl, 1) elseif util.tbl_contains(config.code.ending, el) then table.insert(result, "```") table.remove(tbl, 1) end for name, group in pairs(config.group.detect) do if group and util.tbl_contains(group, el) and string.match(tbl[1], el) and tbl[2] ~= nil then tbl[2] = config.group.styler .. tbl[2] .. config.group.styler if el == tbl[1] then table.remove(tbl, 1) end if control[name] then control[tostring(name)] = false table.insert(result, "---") table.insert(result, "**" .. name .. "**") end end end local ref = require("pretty_hover.parser.references") tbl = ref.check_line_for_references(tbl, config) line = util.join_table(tbl, " ") table.insert(result, line) if insertEmptyLine then table.insert(result, "") end return result end --- Converts a string returned by response.result.contents.value from vim.lsp[textDocument/hover] to markdown. ---@param toConvert string|table Documentation of the string to be converted. ---@param config table Table of options to be used for the conversion to the markdown language. ---@param hl_data table Table of control variables to be used for the pop-up window highlighting. ---@return table Converted table of strings from doxygen to markdown. ---@overload fun(toConvert: string, config: table, hl_data: table): table ---@overload fun(toConvert: table, config: table, hl_data: table): table function M.convert_to_markdown(toConvert, config, hl_data) config.one_liner = false local result = {} local control = {} for name, group in pairs(config.group.detect) do control[tostring(name)] = true end local lines = toConvert if type(toConvert) == "string" then lines = util.split(toConvert, "([^\n]*)\n?") end if #lines == 0 then return result end -- Remove footer padding. The last line is always empty. if lines[#lines] == "" then table.remove(lines, #lines) end for name, _ in pairs(config.hl) do hl_data.lines[tostring(name)] = {} end for _, line in pairs(lines) do local toAdd = M.transform_line(line, config, control, hl_data) vim.list_extend(result, toAdd) for name, group in pairs(hl_data.lines) do if group.detected then group.detected = false table.insert(hl_data.lines[tostring(name)], { line_nr = util.printable_table_size(result) - 2, to = (config.hl[tostring(name)].line and -1 or string.len(hl_data.replacement)) }) end end end -- If the message is only one-liner, remove the code block. -- See issue #24 if #result == 3 and result[#result] == "```" then result = { result[2] } config.one_liner = true end return result end return M ================================================ FILE: lua/pretty_hover/parser/references.lua ================================================ local M = {} local util = require("pretty_hover.core.util") --- Detect if the check line is already in bold. ---@param table_line table Table of words to be checked. ---@return boolean True if the line style is bold, false otherwise. function M.is_bold(table_line) local last_word = table_line[#table_line] return table_line[1]:find("*") == 1 and last_word:find("*") == #last_word-1 end --- Based on the tabled_line markdown representation, this function returns the surrounding string. ---@param tabled_line table Table of words to be checked. ---@param config table Table of options to be used for the conversion to the markdown language. ---@return table The first element of the table is boolean which indicates if the string is already converted. Second element is the surrounding string. function M.get_surround_string(tabled_line, config) if tabled_line and #tabled_line > 0 and M.is_bold(tabled_line) then return { is_brief = true, marker = config.references.styler[2]} else return { is_brief = false, marker = config.references.styler[1]} end end --- Checks the current line on the index if it is an opening reference. ---@param tabled_line table Table of strings representing current line. ---@param index integer Index of the line to be checked. ---@return boolean True if the reference is opening, false otherwise. function M.is_opening_reference(tabled_line, index) if not tabled_line or not tabled_line[index + 1] or not tabled_line[index] then return false; end return (tabled_line[index]:find("[(]") or tabled_line[index+1]:find("[(]")) and not tabled_line[index+1]:find("[)]") end --- Surrounds the reference from the front. If the reference is opened, it is not closed. ---@param tabled_line table Table of strings representing current line. ---@param index integer Index of the word to be checked. ---@param config table Table of options to be used for the conversion to the markdown language. ---@param surround table Table of the surrounding strings. function M.surround_references(tabled_line, index, config, surround) -- Surround the word in brief line. if surround.is_brief then -- End the brief line formatting if possible. if tabled_line[index-1] then tabled_line[index-1] = tabled_line[index-1] .. config.line.styler end -- Start the reference formatting. tabled_line[index] = surround.marker .. tabled_line[index+1] -- End the reference formatting and start the brief line formatting if possible. if tabled_line[index+2] and not surround.openedReference then tabled_line[index] = tabled_line[index] .. surround.marker tabled_line[index+2] = config.line.styler .. tabled_line[index+2] elseif tabled_line[index+2] then -- The reference is opened so we don't add ending reference. else tabled_line[index] = string.sub(tabled_line[index], 1, #tabled_line[index]-2) .. surround.marker if tabled_line[index]:find('[)]') then surround.openedReference = false end end -- Surround the word in non-brief line. else if not tabled_line or not tabled_line[index + 1] or not tabled_line[index] then return; end tabled_line[index] = surround.marker .. tabled_line[index+1] -- End the reference formatting and start the brief line formatting if possible. if not surround.openedReference then tabled_line[index] = tabled_line[index] .. surround.marker end end end --- Close the opened reference if it is opened. ---@param tabled_line table Table of strings representing current line. ---@param index integer Index of the word to be checked. ---@param config table Table of options to be used for the conversion to the markdown language. ---@param surround table Table of the surrounding strings. function M.close_opened_references(tabled_line, index, config, surround) if surround.openedReference and tabled_line[index]:find("[)]") then if surround.is_brief then if tabled_line[index+1] then tabled_line[index] = tabled_line[index] .. surround.marker tabled_line[index+1] = config.line.styler .. tabled_line[index+1] else tabled_line[index] = string.sub(tabled_line[index], 1, #tabled_line[index]-2) .. surround.marker end else tabled_line[index] = tabled_line[index] .. surround.marker end surround.openedReference = false end end --- Detects the HTML style hyperlinks in the line and converts them to markdown. ---@param tabled_line table Line from the hover message split into words. ---@param word string Word to be checked. ---@param index integer Index of the word from the @c tabled_line to be checked. function M.detect_hyper_links(tabled_line, word, index) if word ~= "\\") then -- Handle the case of the link being the last word in the line. styler = word:match("\\" .. styler) ~= nil and styler or "" local link_text = whole_link[3]:match("([%w_:.]+)\\") or link tabled_line[index] = "[" .. link_text .. "](" .. link .. ")" .. styler -- The link is closed in the next part of the line. elseif word:sub(1,4) == "href" then local link_text = whole_link[3]:sub(2) table.remove(tabled_line, index) -- Accumulate all the words until the closing tag. while not tabled_line[index]:match("\\") do link_text = link_text .. " " .. tabled_line[index] table.remove(tabled_line, index) end -- The last word may be a space or just the closing tag. local final_word = tabled_line[index]:match("(%w+)\\") or "" link_text = link_text .. " " .. final_word -- Handle the case of the link being the last word in the line. styler = tabled_line[index]:match("\\" .. styler) ~= nil and styler or "" tabled_line[index] = "[" .. link_text .. "](" .. link .. ")" ..styler end end --- Remove escaping sequence before every character in the escapees string. --- The used escapees are '*' by default. Be sure that adding character to the string will not break --- the highlighting. e.g. adding _ will not do harm in words like __foo but in workds like __foo__. --- The second one will be highlighted as bold. The first one will be shortened to _foo. --- --- @param tabled_line table Line from the hover message split into words. --- @param escapees table? Characters to be removed from the escape sequence. local function remove_excape_characters(tabled_line, escapees) escapees = vim.tbl_deep_extend("force", { "*" }, escapees or {}) local to_escape = table.concat(escapees) for index, word in ipairs(tabled_line) do tabled_line[index] = word:gsub("\\([" .. to_escape .. "])", "%1") end end --- Converts all the references to markdown text. ---@param tabled_line table Words to be checked. ---@param config table Table of options to be used for the conversion to the markdown language. ---@return table Converted line to markdown. function M.check_line_for_references(tabled_line, config) local surround = M.get_surround_string(tabled_line, config) surround.openedReference = false remove_excape_characters(tabled_line) for index, word in ipairs(tabled_line) do if util.tbl_contains(config.references.detect, word) then -- Handle the parenthesis surrounding the reference. if tabled_line[index]:sub(1,1) == "(" then tabled_line[index+1] = "(" .. tabled_line[index+1] end if M.is_opening_reference(tabled_line, index) then surround.openedReference = true end M.surround_references(tabled_line, index, config, surround) table.remove(tabled_line, index + 1) end M.close_opened_references(tabled_line, index, config, surround) M.detect_hyper_links(tabled_line, word, index) -- We cannot use `word` because it will change also the hyperlinks. tabled_line[index] = tabled_line[index]:gsub("\\(<%w+)", "%1") end return tabled_line end return M