diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..077b87e --- /dev/null +++ b/docs/README.md @@ -0,0 +1,7 @@ + + +# Welcome to lsp-progress.nvim's Documentation! + +- [Design & Technologies](/design_and_technologies.md) +- [Advanced Configurations](/advanced_configurations.md) +- [Sponsor](/sponsor.md) diff --git a/docs/_sidebar.md b/docs/_sidebar.md new file mode 100644 index 0000000..0cca329 --- /dev/null +++ b/docs/_sidebar.md @@ -0,0 +1,4 @@ +- [Home](/) +- [Design & Technologies](/design_and_technologies.md) +- [Advanced Configurations](/advanced_configurations.md) +- [Sponsor](/sponsor.md) diff --git a/docs/advanced_configurations.md b/docs/advanced_configurations.md new file mode 100644 index 0000000..6d8c86e --- /dev/null +++ b/docs/advanced_configurations.md @@ -0,0 +1,367 @@ +# Advanced Configurations + +## Show LSP Client Names + + + +Configurations: + +```lua +require("lsp-progress").setup({ + client_format = function(client_name, spinner, series_messages) + if #series_messages == 0 then + return nil + end + return { + name = client_name, + body = spinner .. " " .. table.concat(series_messages, ", "), + } + end, + format = function(client_messages) + --- @param name string + --- @param msg string? + --- @return string + local function stringify(name, msg) + return msg and string.format("%s %s", name, msg) or name + end + + local sign = "" -- nf-fa-gear \uf013 + local lsp_clients = vim.lsp.get_active_clients() + local messages_map = {} + for _, climsg in ipairs(client_messages) do + messages_map[climsg.name] = climsg.body + end + + if #lsp_clients > 0 then + table.sort(lsp_clients, function(a, b) + return a.name < b.name + end) + local builder = {} + for _, cli in ipairs(lsp_clients) do + if + type(cli) == "table" + and type(cli.name) == "string" + and string.len(cli.name) > 0 + then + if messages_map[cli.name] then + table.insert( + builder, + stringify(cli.name, messages_map[cli.name]) + ) + else + table.insert(builder, stringify(cli.name)) + end + end + end + if #builder > 0 then + return sign .. " " .. table.concat(builder, ", ") + end + end + return "" + end, +}) +``` + +## Show LSP Client Counts & Names + + + +Configurations: + +```lua +require("lsp-progress").setup() +``` + +```lua +return require("lsp-progress").progress({ + format = function(messages) + local active_clients = vim.lsp.get_active_clients() + local client_count = #active_clients + if #messages > 0 then + return " LSP:" + .. client_count + .. " " + .. table.concat(messages, " ") + end + if #active_clients <= 0 then + return " LSP:" .. client_count + else + local client_names = {} + for i, client in ipairs(active_clients) do + if client and client.name ~= "" then + table.insert(client_names, "[" .. client.name .. "]") + print( + "client[" .. i .. "]:" .. vim.inspect(client.name) + ) + end + end + return " LSP:" + .. client_count + .. " " + .. table.concat(client_names, " ") + end + end, +}) +``` + +## Use A Check Mark `✓` On Message Complete + +Use a green check mark `✓` on lsp message complete, follow the [fidget.nvim](https://github.com/j-hui/fidget.nvim) style. + +?> Credit: [@ryanmsnyder](https://github.com/ryanmsnyder), see: . + + + +Configurations: + +```lua +-- Create a highlighting group with green color +vim.cmd([[ hi LspProgressMessageCompleted ctermfg=Green guifg=Green ]]) + +require("lsp-progress").setup({ + series_format = function(title, message, percentage, done) + local builder = {} + local has_title = false + local has_message = false + if title and title ~= "" then + table.insert(builder, title) + has_title = true + end + if message and message ~= "" then + table.insert(builder, message) + has_message = true + end + if percentage and (has_title or has_message) then + table.insert(builder, string.format("(%.0f%%%%)", percentage)) + end + if done and (has_title or has_message) then + table.insert(builder, "- done") + end + -- return table.concat(builder, " ") + return { msg = table.concat(builder, " "), done = done } + end, + client_format = function(client_name, spinner, series_messages) + if #series_messages == 0 then + return nil + end + local builder = {} + local done = true + for _, series in ipairs(series_messages) do + if not series.done then + done = false + end + table.insert(builder, series.msg) + end + if done then + -- replace the check mark once done + spinner = "%#LspProgressMessageCompleted#✓%*" + end + return "[" + .. client_name + .. "] " + .. spinner + .. " " + .. table.concat(builder, ", ") + end, +}) +``` + +```lua +local function LspIcon() + local active_clients_count = #vim.lsp.get_active_clients() + return active_clients_count > 0 and " LSP" or "" +end + +local function LspStatus() + return require("lsp-progress").progress({ + format = function(messages) + return #messages > 0 and table.concat(messages, " ") or "" + end, + }) +end + +require('lualine').setup({ + sections = { + lualine_a = { "mode" }, + lualine_b = { + "branch", + "diff", + }, + lualine_c = { + "filename", + "diagnostics", + LspIcon, + LspStatus, + }, + ... + } +}) + +vim.api.nvim_create_augroup("lualine_augroup", { clear = true }) +vim.api.nvim_create_autocmd("User LspProgressStatusUpdated", { + group = "lualine_augroup", + callback = require("lualine").refresh, +}) +``` + +## Put Progress On The Right Side + +Put lsp progress messages on the right side of lualine. + +?> Credit: [@daephx](https://github.com/daephx), see: . + + + +Minimal `init.lua` (Windows 10 x86_64, Neovim v0.9.2): + +```lua +local root = vim.fn.stdpath("data") + +-- bootstrap lazy +local lazypath = root .. "/lazy/plugins/lazy.nvim" +if not vim.loop.fs_stat(lazypath) then + vim.fn.system({ + "git", + "clone", + "--filter=blob:none", + "https://github.com/folke/lazy.nvim.git", + lazypath, + }) +end +vim.opt.runtimepath:prepend(lazypath) + +-- install plugins +local plugins = { + { -- Initialize language server configuration + "neovim/nvim-lspconfig", + cmd = { "LspInfo", "LspInstall", "LspUninstall" }, + event = { "BufReadPost", "BufNewFile" }, + dependencies = { + { "williamboman/mason.nvim", config = true }, + { "folke/neodev.nvim", config = true }, + }, + config = function() + require("lspconfig")["lua_ls"].setup({ + settings = { + Lua = { + diagnostics = { + enable = true, + globals = { "vim" }, + }, + workspace = { + checkThirdParty = false, + }, + }, + }, + }) + end, + }, + { + "jose-elias-alvarez/null-ls.nvim", + event = { "BufReadPost", "BufNewFile" }, + dependencies = { "nvim-lua/plenary.nvim" }, + config = function() + local null_ls = require("null-ls") + null_ls.setup({ + sources = { null_ls.builtins.formatting.stylua }, + }) + end, + }, + { + "nvim-lualine/lualine.nvim", + event = "UIEnter", + dependencies = { + -- Lua fork of vim-web-devicons for neovim + { "nvim-tree/nvim-web-devicons" }, + -- A performant lsp progress status for Neovim. + { + "linrongbin16/lsp-progress.nvim", + config = true, + -- dev = true, + -- dir = "~/github/linrongbin16/lsp-progress.nvim", + }, + }, + config = function(_, opts) + require("lualine").setup(opts) + + vim.api.nvim_create_augroup("lualine_augroup", { clear = true }) + vim.api.nvim_create_autocmd("User LspProgressStatusUpdated", { + group = "lualine_augroup", + callback = require("lualine").refresh, + }) + end, + opts = { + sections = { + lualine_a = { "mode" }, + lualine_b = {}, + lualine_c = { "filename" }, + lualine_x = { + { -- Setup lsp-progress component + function() + return require("lsp-progress").progress({ + max_size = 80, + format = function(messages) + local active_clients = + vim.lsp.get_active_clients() + if #messages > 0 then + return table.concat(messages, " ") + end + local client_names = {} + for _, client in ipairs(active_clients) do + if client and client.name ~= "" then + table.insert( + client_names, + 1, + client.name + ) + end + end + return table.concat(client_names, "  ") + end, + }) + end, + icon = { "", align = "right" }, + }, + "diagnostics", + }, + lualine_y = { "filetype", "encoding", "fileformat" }, + lualine_z = { "location" }, + }, + }, + }, +} + +-- Attach autocmd to enable auto-formatting on save +vim.api.nvim_create_autocmd({ "LspAttach" }, { + callback = function(ev) + -- Apply autocmd if client supports formatting + vim.api.nvim_create_autocmd("BufWritePre", { + buffer = ev.buf, + desc = "Apply Auto-formatting for to document on save", + group = vim.api.nvim_create_augroup("LspFormat." .. ev.buf, {}), + callback = function() + vim.lsp.buf.format({ + bufnr = ev.buf, + filter = function(client) + return client.name == "null-ls" + end, + }) + end, + }) + end, +}) + +-- Setup lazy.nvim +require("lazy").setup(plugins, { + root = root .. "/lazy/plugins", +}) + +``` diff --git a/docs/design_and_technologies.md b/docs/design_and_technologies.md new file mode 100644 index 0000000..6beae39 --- /dev/null +++ b/docs/design_and_technologies.md @@ -0,0 +1,139 @@ +# Design & Technologies + +## The [`$/progress`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#progress) Method + +This plugin registers the `$/progress` method into Neovim's [vim.lsp.handlers](https://neovim.io/doc/user/lsp.html#vim.lsp.handlers) table (or use the `LspProgress` event for Neovim v0.10+). Once there's any lsp progress messages, the registered callback function will be invoked. + +Here I use the [Producer-Consumer Pattern](https://en.wikipedia.org/wiki/Producer%E2%80%93consumer_problem) to split the data processing and UI rendering into two parts: + +- Producer: The lua function that registers on `$/progress` method (or `LspProgress` event). It receives lsp progress messages and saves in plugin context, then produces a user event to notify. +- Consumer: A Vim `autocommand` that listens to the user event, consumes the lsp progress messages, prints to UI component, for example the statusline. + +That's why I provide two APIs: + +- `setup`: It registers a lua callback `function(err, msg, ctx)` on `$/progress` method (or `LspProgress` event). +- `progress`: It consumes the lsp progress messages, and returns the final rendered text contents. + +And one user event: + +- `User LspProgressStatusUpdated`: When there's a lsp progress message, this plugin will emit this event to notify users. + +## Data Structures + +A Vim buffer (the file you're editing) can have multiple lsp cilents, each lsp client can have multiple progress messages (each message has a unique token), each message is a time-based data series from beginning to end, i.e. the _**progress**_. + +Based on this scenario, we have a 2-layer hash table: + +image + +The layer-1 maps from a client ID to a client instance, the layer-2 maps from a unique token to a message instance, i.e. the _**series**_. + +## Performance + +Formatting the 2-layer hash table is a **O(N \* M)** time complexity calculation. + +?> **N** is the active client count, **M** is the unique token count. + +If we format the messages right before the statusline refreshing, it can lead to editor blocking if the data is too big. It actually happened to me, since I installed over 10+ lsp servers and 5+ code formatters, linters, etc through none-ls, for the super large git monorepo (much bigger than [linux kernel](https://github.com/torvalds/linux)) that belongs to the company I'm working for, it contains over 10+ programming languages. + +There're 3 steps to optimize: + +1. Add format cache on both message instances and client instances (the _**red**_ parts): + + image + +2. Split the **O(N \* M)** calculation into each message's updating. Every time a message receives updates, it will: + + 1. On message instance, it invokes the `series_format` function to format the lsp message and cache to its `formatted` variable. + 2. On client instance, it invokes the `client_format` function to concatenate multiple `formatted` caches and cache to its `formatted` variable. + +3. When statusline refreshing, the `progress` API invokes the `format` function to concatenate multiple client instances `formatted` caches and returns a final result, which is quite cheap and fast. + +## Customization + +There're 3 formatting hook functions that maximize the customization. From bottom to top they are: + +### `series_format` + +```lua +--- @param title string? +--- Message title. +--- @param message string? +--- Message body. +--- @param percentage integer? +--- Progress in percentage numbers: 0-100. +--- @param done boolean +--- Indicate whether this series is the last one in progress. +--- @return lsp_progress.SeriesFormatResult +--- The returned value will be passed to function `client_format` as +--- one of the `series_messages` array, or ignored if return nil. +series_format = function(title, message, percentage, done) +``` + +It formats the message level data. The returned value will be passed to next level `client_format` function, as one of `series_messages` array parameter. + +By default the result looks like: + +``` +formatting isort (100%) - done +formatting black (50%) +``` + +### `client_format` + +```lua +--- @param client_name string +--- Client name. +--- @param spinner string +--- Spinner icon. +--- @param series_messages string[]|table[] +--- Messages array. +--- @return lsp_progress.ClientFormatResult +--- The returned value will be passed to function `format` as one of the +--- `client_messages` array, or ignored if return nil. +client_format = function(client_name, spinner, series_messages) +``` + +It formats the client level data, the parameter `series_messages` is an array, each of them is returned from `series_format`. The returned value will be passed to next level `format` function, as one of `client_messages` array parameter. + +By default the result looks like: + +``` +[null-ls] ⣷ formatting isort (100%) - done, formatting black (50%) +``` + +### `format` + +```lua +--- @param client_messages string[]|table[] +--- Client messages array. +--- @return string +--- The returned value will be returned as the result of `progress` API. +format = function(client_messages) +``` + +It formats the top level data, the parameter `client_messages` is an array, each of them is returned from `client_format`. The returned value will be passed as the result of `progress` API. + +By default the result looks like: + +``` + LSP [null-ls] ⣷ formatting isort (100%) - done, formatting black (50%) +``` + +!> There's no such requirements that these formatting functions have to return a `string` type. Actually you can return any type, for example `table`, `array`, `number`. But `nil` value will be ignored and throw away. + +## Other Enhancements + +### Spinning Animation + +The `$/progress` method doesn't guarantee when to update, but user may want an accurate spinning animation, i.e. the `⣷` icon keeps spinning in a fixed rate. A background job is scheduled and runs within a fixed interval time to update the spin icon. + +### Delayed Disappearance + +A lsp progress message can be really fast, appears and disappears in an instant, and people cannot even see it. A delayed timeout is been set to cache the last message for a while. During this while, the spinning animation still needs to keep running! + +### Message Deduplication + +Again in the super large git monorepo, I have seen 4+ duplicated `formatting (100%)` and `diagnostics (100%)` messages showing at the same time, when I work with `eslint`, `prettier` (via `none-ls`) and `flow`. Turns out they come from the multiple processes launched by the `flow-cli` in background, which is quite noisy. + +So I introduced another hash table (maps `title+message` to `token`) to detect the duplicates and reduce them. diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..d981d5c --- /dev/null +++ b/docs/index.html @@ -0,0 +1,49 @@ + + + + + Document + + + + + + +
+ + + + + + + + + diff --git a/docs/sponsor.md b/docs/sponsor.md new file mode 100644 index 0000000..d544364 --- /dev/null +++ b/docs/sponsor.md @@ -0,0 +1,15 @@ +# Sponsor + +Please sponsor me by: + +## GitHub Sponsor + + + +## Alipay + +image + +## Wechat Pay + +image