Extensible spinner framework for Neovim plugins and UI.
- Multiple UI locations:
- Pre-defined
statusline,tabline,winbar,cursor,extmark,cmdline,window-titleandwindow-footer. - Any place you can render a text. see Extend
- Pre-defined
- LSP integration: show spinners for
LspProgressandLspRequest. - Extensible API: start / stop / pause a spinner in your plugins and configurations.
- Configurable spinner patterns: Built-in presets with 70+ patterns
Neovim's UI components do NOT refresh automatically.
So there are 2 ways to make the spinner animate:
- Passive Refresh: Insert a function into the UI component, then refreshes this UI component at regular intervals. eg: statusline/tabline/winbar spinner.
- Active Refresh: Periodically call the API to update the text content of the UI component. eg: cursor/extmark spinner.
spinner.nvim manages the internal state of the spinner and determines when to
refresh the UI. Each spinner is identified by a unique id, with option kind
to indicate how spinner.nvim refresh the UI.
| kind | type | refresh method |
|---|---|---|
| statusline | passive | vim.cmd("redrawstatus) |
| tabline | passive | vim.cmd("redrawtabline) |
| winbar | passive | vim.cmd("redrawstatus) |
| extmark | active | vim.api.nvim_buf_set_extmarks() |
| cursor | active | vim.api.nvim_win_open() + vim.api.nvim_buf_set_lines() |
| cmdline | active | vim.cmd("echo 'text'") |
| window-title | active | vim.api.nvim_win_set_config() |
| window-footer | active | vim.api.nvim_win_set_config() |
| custom | you tell | you tell how, see Extend |
local spinner = require("spinner")
-- Setup a spinner with a unique id.
spinner.config("id", opts)
-- Control spinner as need.
spinner.start("id") -- Start a spinner
spinner.stop("id") -- Stop a spinner
spinner.pause("id") -- Pause a spinner
spinner.reset("id") -- Reset a spinner
spinner.fail("id") -- Fail a spinner (stop & mark as failed)Using lazy.nvim:
{
"xieyonn/spinner.nvim",
config = function()
---@type spinner
local sp = require("spinner")
-- NO need to call setup() if you are fine with defaults.
sp.setup()
end
}Using rocks.nvim
:Rocks install spinner.nvimType in command line:
:= require("spinner.demo").open()Will open a window displaying all built-in spinners.
Setup defaults for spinners.
require("spinner").setup({
-- Pre-defined pattern key name in
-- https://github.com/xieyonn/spinner.nvim/blob/main/lua/spinner/pattern.lua
-- or be a table { intervals = 80, frames = { "1", "2" } }
pattern = "dots",
-- Time-to-live in milliseconds since the most recent start, after which the
-- spinner stops, preventing it from running indefinitely.
ttl_ms = 0,
-- Milliseconds to wait after startup before showing the spinner.
-- This helps prevent the spinner from briefly flashing for short-lived tasks.
initial_delay_ms = 0,
-- Text displayed when the spinner is idle/stoped.
--
-- true: show an empty string, with length equal to spinner frames.
-- false: equals to "".
-- or string value, eg: show âś” when lsp progress finished.
-- or a table value, eg: { init = "", stopped = "", failed = "" }
placeholder = false,
-- Highlight group for text, use fg of `Comment` by default.
-- or be a table { init = "", running = "", paused = "", stopped = "", failed = ""}
hl_group = "Spinner",
cursor_spinner = {
-- CursorSpinner window option.
winblend = 60,
-- CursorSpinner window option.
zindex = 50,
-- CursorSpinner window position, relative to cursor.
-- row = -1 col = 1 means Above-Right.
row = -1,
col = 1,
}
})- Display lsp client name (lua_ls) and lsp progress in
statusline. - Display spinner right above cursor when press
K(lsp_hover)
Click to expand
- setup a
statuslinespinner with idlsp_progressand attach toLspProgress
require("spinner").config("lsp_progress", {
kind = "statusline", -- spinner kind.
placeholder = "âś”", -- a nice symbol to indicate lsp is ready.
attach = {
lsp = {
progress = true, -- attach to LspProgress event.
},
},
})- create a global function to concat lsp client names and render spinner text.
if you setup statusline by
vim.o.statusline, you need a global function. or if you use a statusline plugin, you can set this function local.
function lsp_progress()
local client_names = {}
local seen = {}
-- need to remove duplicate because somehow a buffer may attached to multiple
-- clients with same name.
for _, client in ipairs(vim.lsp.get_clients({ bufnr = 0 })) do
local name = client and client.name or ""
if name ~= "" and not seen[name] then
table.insert(client_names, name)
seen[name] = true
end
end
-- if no active lsp clients, leave it empty
if #client_names == 0 then
return ""
end
local spinner = require("spinner").render("lsp_progress")
return table.concat(client_names, " ") .. " " .. spinner
end- setup statusline
vim.o.statusline = vim.o.statusline .. "%{v:lua.lsp_progress()%}"- setup a
cursorspinner with idcursorand attach toLspRequest
require("spinner").config("cursor", {
kind = "cursor", -- kind cursor
attach = {
lsp = {
request = {
-- select the methods you're interested in. For a complete list: `:h lsp-method`
-- "textDocument/definition", -- for GoToDefinition (shortcut `C-]`)
"textDocument/hover", -- for hover (shortcut `K`)
},
},
},
})- Display a TODO like list in buffer.
- Tracking list status.
Click to expand
local sp = require("spinner")
-- 1. Create a scratch buffer
local bufnr = vim.api.nvim_create_buf(false, true)
vim.bo[bufnr].buftype = "nofile"
vim.bo[bufnr].bufhidden = "wipe"
vim.bo[bufnr].swapfile = false
vim.bo[bufnr].undofile = false
-- 2. Open a window
local width = math.floor(vim.o.columns * 0.4)
local height = math.floor(vim.o.lines * 0.6)
local row = math.floor((vim.o.lines - height) / 2)
local col = math.floor((vim.o.columns - width) / 2)
local win = vim.api.nvim_open_win(bufnr, true, {
relative = "editor",
row = row,
col = col,
width = width,
height = 8,
style = "minimal",
focusable = true,
border = "rounded",
noautocmd = false,
})
-- 3. close window on q or esc
for _, key in ipairs({ "q", "<Esc>" }) do
vim.keymap.set("n", key, function()
if vim.api.nvim_win_is_valid(win) then
vim.api.nvim_win_close(win, true)
end
end, {
buffer = bufnr,
nowait = true,
silent = true,
})
end
-- 4. render a list
local todolist = {
" Item 1",
" Item 2",
" Item 3",
" Item 4",
}
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, todolist)
for i = 1, #todolist do
sp.config("todo" .. i, {
kind = "extmark",
pattern = "blink",
placeholder = "â—Ź",
bufnr = bufnr,
row = i - 1,
col = 1,
ttl_ms = 4000,
hl_group = {
init = "white", -- init hl_group
stopped = "green", -- stopped hl_group
},
virt_text_pos = "overlay",
})
end
-- 5. start spinner
for i = 1, #todolist do
vim.defer_fn(function()
sp.start("todo" .. i)
end, i * 1000)
end- Configure a
statuslinespinner with idmy_spinner.
require("spinner").config("my_spinner", {
kind = "statusline",
})- Set
vim.o.statusline.
-- Need a global function here.
function my_spinner()
return require("spinner").render("my_spinner")
end
vim.o.statusline = vim.o.statusline .. "%{v:lua.my_spinner()%}"- Start/stop/pause the spinner where needed.
-- start spinner
require("spinner").start("my_spinner")
-- stop spinner
require("spinner").stop("my_spinner")
-- spinner.nvim internally tracks the number of start/stop calls for the same
-- spinner. It only stops spinning when the number of stop calls >= start calls.
-- use a second param `true` to force stop a spinner.
require("spinner").stop("my_spinner", true)
-- pause a spinner
require("spinner").pause("my_spinner")By default, spinner.nvim refreshes the statusline using vim.cmd("redrawstatus").
If you use a statusline plugin, it may take over the refresh mechanism, causing
the spinner to behave incorrectly due to refresh/frame-rate issues.
You can use the on_update_ui option to let your statusline plugin handle updates properly.
eg: if you use lualine.nvim
require("spinner").config("my_spinner", {
kind = "statusline",
on_update_ui = function()
require("lualine").refresh() -- use lualine's refresh method
end,
})lualine.nvim is auto-detect and support by default.
- Configure a
tablinespinner with idmy_spinner.
require("spinner").config("my_spinner", {
kind = "tabline",
})- Set
vim.o.tabline.
-- this function need to be a global function
function my_spinner()
return require("spinner").render("my_spinner")
end
vim.o.tabline = vim.o.tabline .. "%{v:lua.my_spinner()%}"spinner.nvim use
vim.cmd("redrawtabline")to refresh tabline. if you use a plugin to setup tabline, you may need to provide aon_update_uioption to refresh tabline. See Statusline.
- Configure a
winbarspinner with idmy_spinner.
require("spinner").config("my_spinner", {
kind = "winbar",
})- Set
vim.o.winbar.
-- this function need to be a global function
function my_spinner()
return require("spinner").render("my_spinner")
end
vim.o.winbar = vim.o.winbar .. "%{v:lua.my_spinner()%}"spinner.nvim use
vim.cmd("redrawstatus")to refresh winbar. if you use a plugin to setup winbar, you may need to provide aon_update_uioption to refresh tabline. See Statusline.
spinner.nvim use a float window relative to cursor to displaying spinner.
create the float window when spinner start/pause, close the float window when stop.
If you want to show multiple
cursorspinners, be careful with id.
Configure a cursor spinner with id.
require("spinner").config("cursor", {
kind = "cursor",
-- CursorSpinner window option.
winblend = 60, -- optional
-- CursorSpinner window option.
zindex = 50, --optional
-- CursorSpinner window position, relative to cursor.
row = -1, --optional
-- CursorSpinner window position, relative to cursor.
col = 1, --optional
})row and col means the position relative to cursor.
{ row = -1, col = 1 }Above-Right{ row = -1, col = -1 }Above-Left{ row = 1, col = 1 }Below-Right{ row = 1, col = -1 }Below-Left
spinner.nvim uses Neovim extmarks (see h: extmark) to attach spinners to buffer
positions.
Extmarks automatically track positions as the text changes, ensuring the spinner stays correctly aligned even when you edit the buffer, like diagnostic messages.
If you want to show multiple spinners at the same time, be careful with the spinner id.
Configure an extmark spinner with id.
local bufnr, row, col
local id = string.format("extmark-spinner-%d-%d-%d", bufnr, row, col)
require("spinner").config(id, {
kind = "extmark",
bufnr = bufnr, -- must be provided
row = row, -- must be provided, which line, 0-based
col = col, -- must be provided, which col, 0-based
ns = 0, -- namespace, optional
virt_text_pos = "eol" -- options for `vim.api.nvim_buf_set_extmark`, optional
virt_text_win_col = nil -- options for `vim.api.nvim_buf_set_extmarks`, optional
})virt_text_pos and virt_text_win_col determine spinner position.
| virt_text_pos | virt_text_win_col | Description |
|---|---|---|
| overlay | nil | Draws at the col anchor |
| overlay | integer | Draws at the specified window column, ignoring col |
| eol | nil | Draws at the end of the line |
| eol | integer | virt_text_win_col is ignored |
| right_align | nil | Aligns text to the right of the window |
| right_align | integer | virt_text_win_col is ignored |
| inline | nil | Inserts at the specified col |
| inline | integer | virt_text_win_col is ignored |
See
h: nvim_buf_set_extmarks().
Configure a cmdline spinner with id my_spinner.
require("spinner").config("my_spinner", {
kind = "cmdline"
fmt = function(event)
local text = event.text
return text .. " Loading .."
end
})A preview
Configure a window-title spinner:
local win = nil -- target win
local id = string.format("window-title:%d", win)
require("spinner").config(id, {
kind = "window-title",
win = win, -- target win, must provided
pos = "center", -- optional, can be "left", "center", "right", default is "center"
-- optional, use fmt function to add extra text.
fmt = function(event)
local text = event.text
return text .. " This is a title"
end,
})Spinner will stop when window close.
A preview:
Configure a window-footer spinner:
local win = nil -- target win
local id = string.format("window-footer:%d", win)
require("spinner").config(id, {
kind = "window-footer",
win = win, -- target win, must provided
pos = "center", -- optional, can be "left", "center", "right", default is "center"
-- optional, use fmt function to add extra text.
fmt = function(event)
local text = event.text
return text .. " This is a footer"
end,
})Spinner will stop when window close.
A preview:
spinner.nvim decides when to refresh the UI, you decide where and how.
Extend a spinner using the on_update_ui option, which is a function that
gets called every time the UI needs to be refreshed, including:
- init: when config/reset
- running: When spinning
- pause: When paused
- stop: When stopped
- fail: When failed
if spinner has initial_delay_ms option, only refresh ui when timeout expires.
local id = "my_spinner"
require("spinner").config(id, {
kind = "custom",
-- must provide, called when refresh UI.
on_update_ui = function(event)
local status = event.status -- current status
local text = event.text -- current text (including placeholder)
local hl_group = event.hl_group -- current hl_group if provided
-- do what you want
end,
-- optional, used to improve performance, take spinner id by default
ui_scope = id,
})Option ui_scope defines the scope for batching UI updates.
Spinners with the same ui_scope will have their UI updates combined within a
short period of time to improve performance.
- All
statuslinespinners share thestatuslinescope and update together. - All
tablinespinners share thetablinescope and update together. - Custom spinners with the same
ui_scopewill update together.
Here is a example shows how to display spinner in a window title. (only float window can set title)
local bufnr = vim.api.nvim_create_buf(false, true)
local win = vim.api.nvim_open_win(bufnr, true, {})
local spinner_id = string.format("win-%d", win)
require("spinner").config(spinner_id, {
kind = "custom",
ui_scope = spinner_id, -- Tell spinner.nvim not to merge refreshes with other spinners.
on_update_ui = function(event)
if not (win and vim.api.nvim_win_is_valid(win)) then
return
end
vim.api.nvim_win_set_config(win, {
title = event.text,
title_pos = "center",
})
end,
})
-- start spinner
require("spinner").start(spinner_id)This is how the built-in window-title spinner is implemented.
Configure spinner with attach option to make it listen to LspProgress or LspRequest.
require("spinner").config("cursor", {
kind = "cursor",
attach = {
lsp = {
progress = true, -- listen to LspProgress
request = {
-- Select the methods you're interested in. For a complete list: `:h lsp-method`
-- "textDocument/definition", -- for GoToDefinition (shortcut `C-]`)
"textDocument/hover", -- for hover (shortcut `K`)
},
-- Optional, select lsp names you're interested in.
client_names = {
"lua_ls",
}
},
},
})spinner.nvim provides a large number of built-in patterns. See patterns
Or you can set up a custom pattern:
require("spinner").config("my_spinner", {
kind = "statusline",
pattern = {
interval = 80, -- in milliseconds
frames = { "â ‹", "â ™", "â ą", "â ¸", "â Ľ", "â ´", "â ¦", "â §", "â ‡", "â Ź" },
},
})Tasks may fail or abort unexpectedly, causing the stop signal to never be received.
You can set a ttl_ms to prevent the spinner from spinning indefinitely.
require("spinner").config("my_spinner", {
kind = "statusline",
ttl_ms = 10000, -- 10 seconds
})Some asynchronous tasks are too short: the spinner stops shortly after starting,
causing flickering. Use initial_delay_ms to delay the spinner startup and avoid
flickering.
require("spinner").config("my_spinner", {
kind = "statusline",
initial_delay_ms = 200, -- in milliseconds
})Set a default string to display when the spinner is idle (init/stopped).
require("spinner").config("my_spinner", {
kind = "statusline",
-- eg: show âś” when lsp progress is ready.
placeholder = "âś”",
})placeholder = truerender an empty string with length ==len(frames[1]).placeholder = falsedisable placeholder.placeholder = ""equals tofalse.
You can also set it to a table to control the placeholder displayed during
initialization and stop of the spinner.
require("spinner").config("my_spinner", {
kind = "statusline"
-- eg: show âś” when lsp progress is ready.
placeholder = {
init = " ", -- show empty if not started.
stopped = "âś”", -- show âś” when success.
failed = "âś—" -- show âś— when failed.
},
})You can customize the spinner text format using the fmt option. This function
receives the current text and status and returns the formatted string.
require("spinner").config("my_spinner", {
kind = "statusline",
fmt = function(event)
local text = event.text
-- local status = event.status
-- local hl_group = event.hl_group
return "[" .. text .. "]"
end
})event.text will contains hl_group ONLY in statusline/tabline/winbar/cmdline spinner. You are free to add other hl_group for text in other spinners.
You can customize the spinner text color by using the hl_group option.
require("spinner").config("my_spinner", {
kind = "statusline",
hl_group = "Spinner"
})Or set a global default value in setup()
require("spinner").setup({
hl_group = "Spinner",
})You can set it to a table to display different colors for different spinner states.
require("spinner").config("my_spinner", {
kind = "statusline",
hl_group = {
init = "", -- after spinner config
running = "", -- running
paused = "", -- paused
stopped = "", -- stopped
failed = "", -- failed
}
})eg: show a gree âś” to indicate lsp is ready.
statusline / tabline / winbar will wrap text in format
%#HL_GROUP#...%*and requires explicit setting inconfig()rather thensetup()for backward compatibility
spinner.nvim provides a command Spinner:
:Spinner start my_spinner " Start a spinner
:Spinner stop my_spinner " Stop a spinner
:Spinner pause my_spinner " Pause a spinner
:Spinner reset my_spinner " Reset a spinner
:Spinner fail my_spinner " Fail a spinnerWith tab completion for spinner IDs.
Use it to test your configuration.
Click to expand
---@class spinner
---@field start fun(id: string) -- Start spinner.
---@field stop fun(id: string, force?: boolean) -- Stop spinner.
---@field pause fun(id: string) -- Pause spinner.
---@field config fun(id: string, opts?: spinner.Opts) -- Setup spinner.
---@field render fun(id: string): string -- Render spinner.
---@field reset fun(id: string) -- Reset spinner.
---@field fail fun(id: string) -- Fail spinner, stop & mark status as failed.
---@field setup fun(opts?: spinner.Config) -- Setup global configuration.
---@alias spinner.UIScope -- Used to combine UI updates.
---| "statusline"
---| "tabline"
---| "cursor"
---| string
---@alias spinner.Kind
---| "custom" -- Custom UI kind
---| "statusline" -- Statusline spinner
---| "tabline" -- Tabline spinner
---| "winbar" -- Winbar spinner
---| "cursor" -- Cursor spinner
---| "extmark" -- Extmark spinner
---| "cmdline" -- CommandLine spinner
---| "window-title" -- WindowTitle spinner
---| "window-footer" -- WindowFooter spinner
---@alias spinner.Opts
---| spinner.CoreOpts -- Core options
---| spinner.CustomOpts -- Custom options
---| spinner.StatuslineOpts -- Statusline options
---| spinner.TablineOpts -- Tabline options
---| spinner.WinbarOpts -- Winbar options
---| spinner.CursorOpts -- Cursor options
---| spinner.ExtmarkOpts -- Extmark options
---| spinner.CmdlineOpts -- CommandLine options
---| spinner.WindowTitleOpts -- WindowTitle options
---| spinner.WindowFooterOpts -- WindowFooter options
---
---@class spinner.CoreOpts
---@field kind? spinner.Kind -- Spinner kind
---@field pattern? string|spinner.Pattern -- Animation pattern
---@field ttl_ms? integer -- Time to live in ms
---@field initial_delay_ms? integer -- Initial delay in ms
---@field placeholder? string|boolean|spinner.Placeholder -- Placeholder text
---@field attach? spinner.Event -- Event attachment
---@field hl_group? string|spinner.HighlightGroup
---@field on_update_ui? fun(event: spinner.OnChangeEvent) -- UI update callback
---@field ui_scope? string custom ui_scope, used to improve UI refresh performance
---@field fmt? fun(event: spinner.OnChangeEvent): string -- Format function
---
---@class spinner.Placeholder
---@field init? string -- when status == init (new create)
---@field stopped? string -- when status == stopped
---@field failed? string -- when status == failed
---
---@class spinner.StatuslineOpts: spinner.CoreOpts
---@field kind "statusline" -- Statusline kind
---
---@class spinner.TablineOpts: spinner.CoreOpts
---@field kind "tabline" -- Tabline kind
---
---@class spinner.WinbarOpts: spinner.CoreOpts
---@field kind "winbar" -- Winbar kind
---
---@class spinner.CursorOpts: spinner.CoreOpts
---@field kind "cursor" -- Cursor kind
---@field row? integer -- Position relative to cursor
---@field col? integer -- Position relative to cursor
---@field zindex? integer -- Z-index
---@field winblend? integer -- Window blend
---
---@class spinner.HighlightGroup
---@field init? string -- used in init status
---@field running? string -- used in running status
---@field paused? string -- used in paused status
---@field stopped? string -- used in stopped status
---@field failed? string -- used in failed status
---
---@class spinner.ExtmarkOpts: spinner.CoreOpts
---@field kind "extmark" -- Extmark kind
---@field bufnr integer -- Buffer number
---@field row integer -- Line position 0-based
---@field col integer -- Column position 0-based
---@field ns? integer -- Namespace
---@field virt_text_pos? string -- options for vim.api.nvim_buf_set_extmark
---@field virt_text_win_col? integer -- options for `vim.api.nvim_buf_set_extmarks`
---
---@class spinner.WindowTitleOpts: spinner.CoreOpts
---@field kind "window-title"
---@field win integer -- target win id
---@field pos? string -- position, can be on of "left", "center" or "right"
---
---@class spinner.WindowFooterOpts: spinner.CoreOpts
---@field kind "window-footer"
---@field win integer -- target win id
---@field pos? string -- position, can be on of "left", "center" or "right"
---
---@class spinner.CmdlineOpts: spinner.CoreOpts
---@field kind "cmdline" -- CommandLine kind
---
---@class spinner.CustomOpts: spinner.CoreOpts
---@field kind "custom"
---@field on_update_ui fun(event: spinner.OnChangeEvent) -- UI update callback
---@field ui_scope? string custom ui_scope, use spinner id by default
---
---@class spinner.OnChangeEvent
---@field status spinner.Status -- Current status
---@field text string -- Current text
---@field hl_group? string -- Current hl_group
---@enum spinner.Status
local STATUS = {
INIT = "init", -- Initialized but not started status
DELAYED = "delayed", -- Delayed status
RUNNING = "running", -- Running status
PAUSED = "paused", -- Paused status
STOPPED = "stopped", -- Stopped status
FAILED = "failed", -- Failed state
}
---@class spinner.Pattern
---@field interval integer -- Animation interval
---@field frames string[] -- Animation frames
---@class spinner.Event
---@field lsp? spinner.Event.Lsp -- LSP event attachment
---
---@class spinner.Event.Lsp
---@field client_names? string[] -- Client names filter
---@field progress? boolean -- Progress event
---@field request? spinner.LspRequest[] -- Request events
---
---@alias spinner.LspRequest
---| 'callHierarchy/incomingCalls'
---| 'callHierarchy/outgoingCalls'
---| 'codeAction/resolve'
---| 'codeLens/resolve'
---| 'completionItem/resolve'
---| 'documentLink/resolve'
---| 'initialize'
---| 'inlayHint/resolve'
---| 'shutdown'
---| 'textDocument/codeAction'
---| 'textDocument/codeLens'
---| 'textDocument/colorPresentation'
---| 'textDocument/completion'
---| 'textDocument/declaration'
---| 'textDocument/definition'
---| 'textDocument/diagnostic'
---| 'textDocument/documentColor'
---| 'textDocument/documentHighlight'
---| 'textDocument/documentLink'
---| 'textDocument/documentSymbol'
---| 'textDocument/foldingRange'
---| 'textDocument/formatting'
---| 'textDocument/hover'
---| 'textDocument/implementation'
---| 'textDocument/inlayHint'
---| 'textDocument/inlineCompletion'
---| 'textDocument/inlineValue'
---| 'textDocument/linkedEditingRange'
---| 'textDocument/moniker'
---| 'textDocument/onTypeFormatting'
---| 'textDocument/prepareCallHierarchy'
---| 'textDocument/prepareRename'
---| 'textDocument/prepareTypeHierarchy'
---| 'textDocument/rangeFormatting'
---| 'textDocument/rangesFormatting'
---| 'textDocument/references'
---| 'textDocument/rename'
---| 'textDocument/selectionRange'
---| 'textDocument/semanticTokens/full'
---| 'textDocument/semanticTokens/full/delta'
---| 'textDocument/semanticTokens/range'
---| 'textDocument/signatureHelp'
---| 'textDocument/typeDefinition'
---| 'textDocument/willSaveWaitUntil'
---| 'typeHierarchy/subtypes'
---| 'typeHierarchy/supertypes'
---| 'workspaceSymbol/resolve'
---| 'workspace/diagnostic'
---| 'workspace/executeCommand'
---| 'workspace/symbol'
---| 'workspace/textDocumentContent'
---| 'workspace/willCreateFiles'
---| 'workspace/willDeleteFiles'
---| 'workspace/willRenameFiles'
---@class spinner.Config
---@field pattern? string|spinner.Pattern -- Default pattern
---@field ttl_ms? integer -- Default TTL
---@field initial_delay_ms? integer -- Default delay
---@field placeholder? string|boolean|spinner.Placeholder -- Placeholder text
---@field hl_group? string|spinner.HighlightGroup
---@field cursor_spinner? spinner.CursorSpinnerConfig -- Default cursor config
---
---@class spinner.CursorSpinnerConfig
---@field winblend? integer -- Default window blend
---@field zindex? integer -- Default z-index
---@field row? integer -- Default row offset 0-based
---@field col? integer -- Default column offset 0-basedPlease see CONTRIBUTING.md for detailed guidelines on how to contribute to this project.
- cli-spinners Adopted a lot of spinner patterns from there.
- panvimdoc Use this plugin to generate vim docs.
- nvim-dap Borrow
splitstrfunction used in cmdline command completion.