Regenerate nvim config
This commit is contained in:
@ -0,0 +1,18 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Cmdline = require("noice.ui.cmdline")
|
||||
local Status = require("noice.api.status")
|
||||
|
||||
local M = {}
|
||||
|
||||
M.status = Status
|
||||
|
||||
---@deprecated
|
||||
M.statusline = Status
|
||||
|
||||
---@return CmdlinePosition?
|
||||
function M.get_cmdline_position()
|
||||
return Cmdline.position and vim.deepcopy(Cmdline.position)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,66 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Manager = require("noice.message.manager")
|
||||
local Config = require("noice.config")
|
||||
|
||||
---@type NoiceFilter
|
||||
local nothing = { ["not"] = {} }
|
||||
|
||||
---@param str string
|
||||
local function escape(str)
|
||||
return str:gsub("%%", "%%%%")
|
||||
end
|
||||
|
||||
---@param name string
|
||||
---@return NoiceStatus
|
||||
local function NoiceStatus(name)
|
||||
local function _get()
|
||||
if not Config.is_running() then
|
||||
return
|
||||
end
|
||||
local filter = Config.options.status[name] or nothing
|
||||
return Manager.get(filter, {
|
||||
count = 1,
|
||||
sort = true,
|
||||
history = true,
|
||||
})[1]
|
||||
end
|
||||
---@class NoiceStatus
|
||||
return {
|
||||
has = function()
|
||||
return _get() ~= nil
|
||||
end,
|
||||
get = function()
|
||||
local message = _get()
|
||||
if message then
|
||||
return escape(vim.trim(message:content()))
|
||||
end
|
||||
end,
|
||||
get_hl = function()
|
||||
local message = _get()
|
||||
if message and message._lines[1] then
|
||||
local ret = ""
|
||||
local line = message._lines[#message._lines]
|
||||
for _, text in ipairs(line._texts) do
|
||||
if text.extmark and text.extmark.hl_group then
|
||||
-- use hl_group
|
||||
ret = ret .. "%#" .. text.extmark.hl_group .. "#" .. escape(text:content())
|
||||
else
|
||||
-- or reset to StatusLine
|
||||
ret = ret .. "%#StatusLine#" .. escape(text:content())
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
---@type table<string, NoiceStatus>
|
||||
local status = {}
|
||||
|
||||
return setmetatable(status, {
|
||||
__index = function(_, key)
|
||||
return NoiceStatus(key)
|
||||
end,
|
||||
})
|
||||
@ -0,0 +1,110 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local View = require("noice.view")
|
||||
local Manager = require("noice.message.manager")
|
||||
local Config = require("noice.config")
|
||||
local Util = require("noice.util")
|
||||
local Message = require("noice.message")
|
||||
local Router = require("noice.message.router")
|
||||
|
||||
---@class NoiceCommand: NoiceRouteConfig
|
||||
---@field filter_opts NoiceMessageOpts
|
||||
|
||||
local M = {}
|
||||
|
||||
---@type table<string, fun()>
|
||||
M.commands = {}
|
||||
|
||||
---@param command NoiceCommand
|
||||
function M.command(command)
|
||||
return function()
|
||||
local view = View.get_view(command.view, command.opts)
|
||||
view:set(Manager.get(
|
||||
command.filter,
|
||||
vim.tbl_deep_extend("force", {
|
||||
history = true,
|
||||
sort = true,
|
||||
}, command.filter_opts or {})
|
||||
))
|
||||
view:display()
|
||||
end
|
||||
end
|
||||
|
||||
function M.cmd(cmd)
|
||||
if M.commands[cmd] then
|
||||
M.commands[cmd]()
|
||||
else
|
||||
M.commands.history()
|
||||
end
|
||||
end
|
||||
|
||||
function M.setup()
|
||||
M.commands = {
|
||||
debug = function()
|
||||
Config.options.debug = not Config.options.debug
|
||||
end,
|
||||
dismiss = function()
|
||||
Router.dismiss()
|
||||
end,
|
||||
log = function()
|
||||
vim.cmd.edit(Config.options.log)
|
||||
end,
|
||||
enable = function()
|
||||
require("noice").enable()
|
||||
end,
|
||||
disable = function()
|
||||
require("noice").disable()
|
||||
end,
|
||||
telescope = function()
|
||||
require("telescope").extensions.noice.noice({})
|
||||
end,
|
||||
stats = function()
|
||||
Manager.add(Util.stats.message())
|
||||
end,
|
||||
routes = function()
|
||||
local message = Message("noice", "debug")
|
||||
message:set(vim.inspect(Config.options.routes))
|
||||
Manager.add(message)
|
||||
end,
|
||||
config = function()
|
||||
local message = Message("noice", "debug")
|
||||
message:set(vim.inspect(Config.options))
|
||||
Manager.add(message)
|
||||
end,
|
||||
viewstats = function()
|
||||
local message = Message("noice", "debug")
|
||||
message:set(vim.inspect(require("noice.message.router").view_stats()))
|
||||
Manager.add(message)
|
||||
end,
|
||||
}
|
||||
|
||||
for name, command in pairs(Config.options.commands) do
|
||||
M.commands[name] = M.command(command)
|
||||
end
|
||||
|
||||
vim.api.nvim_create_user_command("Noice", function(args)
|
||||
local cmd = vim.trim(args.args or "")
|
||||
M.cmd(cmd)
|
||||
end, {
|
||||
nargs = "?",
|
||||
desc = "Noice",
|
||||
complete = function(_, line)
|
||||
if line:match("^%s*Noice %w+ ") then
|
||||
return {}
|
||||
end
|
||||
local prefix = line:match("^%s*Noice (%w*)") or ""
|
||||
return vim.tbl_filter(function(key)
|
||||
return key:find(prefix) == 1
|
||||
end, vim.tbl_keys(M.commands))
|
||||
end,
|
||||
})
|
||||
|
||||
for name in pairs(M.commands) do
|
||||
local cmd = "Noice" .. name:sub(1, 1):upper() .. name:sub(2)
|
||||
vim.api.nvim_create_user_command(cmd, function()
|
||||
M.cmd(name)
|
||||
end, { desc = "Noice " .. name })
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,54 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Config = require("noice.config")
|
||||
local Highlights = require("noice.config.highlights")
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.setup()
|
||||
local formats = Config.options.cmdline.format
|
||||
for name, format in pairs(formats) do
|
||||
if format == false then
|
||||
formats[name] = nil
|
||||
else
|
||||
local kind = format.kind or name
|
||||
local kind_cc = kind:sub(1, 1):upper() .. kind:sub(2)
|
||||
|
||||
local hl_group_icon = "CmdlineIcon" .. kind_cc
|
||||
Highlights.add(hl_group_icon, "NoiceCmdlineIcon")
|
||||
|
||||
local hl_group_border = "CmdlinePopupBorder" .. kind_cc
|
||||
Highlights.add(hl_group_border, "NoiceCmdlinePopupBorder")
|
||||
|
||||
format = vim.tbl_deep_extend("force", {
|
||||
conceal = format.conceal ~= false,
|
||||
kind = kind,
|
||||
icon_hl_group = "Noice" .. hl_group_icon,
|
||||
view = Config.options.cmdline.view,
|
||||
lang = format.lang or format.ft,
|
||||
opts = {
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
border = {
|
||||
text = {
|
||||
top = format.title or (" " .. kind_cc .. " "),
|
||||
},
|
||||
},
|
||||
win_options = {
|
||||
winhighlight = {
|
||||
FloatBorder = "Noice" .. hl_group_border,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, { opts = vim.deepcopy(Config.options.cmdline.opts) }, format)
|
||||
formats[name] = format
|
||||
|
||||
table.insert(Config.options.routes, {
|
||||
view = format.view,
|
||||
opts = format.opts,
|
||||
filter = { event = "cmdline", kind = format.kind },
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,129 @@
|
||||
local M = {}
|
||||
|
||||
---@type table<string, NoiceFormat>
|
||||
M.builtin = {
|
||||
default = { "{level} ", "{title} ", "{message}" },
|
||||
notify = { "{message}" },
|
||||
details = {
|
||||
"{level} ",
|
||||
"{date} ",
|
||||
"{event}",
|
||||
{ "{kind}", before = { ".", hl_group = "NoiceFormatKind" } },
|
||||
" ",
|
||||
"{title} ",
|
||||
"{cmdline} ",
|
||||
"{message}",
|
||||
},
|
||||
telescope = {
|
||||
"{level} ",
|
||||
"{date} ",
|
||||
"{title} ",
|
||||
"{message}",
|
||||
},
|
||||
telescope_preview = {
|
||||
"{level} ",
|
||||
"{date} ",
|
||||
"{event}",
|
||||
{ "{kind}", before = { ".", hl_group = "NoiceFormatKind" } },
|
||||
"\n",
|
||||
"{title}\n",
|
||||
"\n",
|
||||
"{message}",
|
||||
},
|
||||
lsp_progress = {
|
||||
{
|
||||
"{progress} ",
|
||||
key = "progress.percentage",
|
||||
contents = {
|
||||
{ "{data.progress.message} " },
|
||||
},
|
||||
},
|
||||
"({data.progress.percentage}%) ",
|
||||
{ "{spinner} ", hl_group = "NoiceLspProgressSpinner" },
|
||||
{ "{data.progress.title} ", hl_group = "NoiceLspProgressTitle" },
|
||||
{ "{data.progress.client} ", hl_group = "NoiceLspProgressClient" },
|
||||
},
|
||||
lsp_progress_done = {
|
||||
{ "✔ ", hl_group = "NoiceLspProgressSpinner" },
|
||||
{ "{data.progress.title} ", hl_group = "NoiceLspProgressTitle" },
|
||||
{ "{data.progress.client} ", hl_group = "NoiceLspProgressClient" },
|
||||
},
|
||||
}
|
||||
|
||||
---@class NoiceFormatOptions
|
||||
M.defaults = {
|
||||
---@class NoiceFormatOptions.debug
|
||||
debug = {
|
||||
enabled = true,
|
||||
},
|
||||
---@class NoiceFormatOptions.cmdline
|
||||
cmdline = {},
|
||||
---@class NoiceFormatOptions.level
|
||||
level = {
|
||||
hl_group = {
|
||||
trace = "NoiceFormatLevelTrace",
|
||||
debug = "NoiceFormatLevelDebug",
|
||||
info = "NoiceFormatLevelInfo",
|
||||
warn = "NoiceFormatLevelWarn",
|
||||
error = "NoiceFormatLevelError",
|
||||
off = "NoiceFormatLevelOff",
|
||||
},
|
||||
icons = { error = " ", warn = " ", info = " " },
|
||||
},
|
||||
---@class NoiceFormatOptions.progress
|
||||
progress = {
|
||||
---@type NoiceFormat
|
||||
contents = {},
|
||||
width = 20,
|
||||
align = "right",
|
||||
key = "progress", -- key in message.opts For example: "progress.percentage"
|
||||
hl_group = "NoiceFormatProgressTodo",
|
||||
hl_group_done = "NoiceFormatProgressDone",
|
||||
},
|
||||
---@class NoiceFormatOptions.text
|
||||
text = {
|
||||
text = nil,
|
||||
hl_group = nil,
|
||||
},
|
||||
---@class NoiceFormatOptions.spinner
|
||||
spinner = {
|
||||
---@type Spinner
|
||||
name = "dots",
|
||||
hl_group = nil,
|
||||
},
|
||||
---@class NoiceFormatOptions.data
|
||||
data = {
|
||||
key = nil, -- Key in the message.opts object.
|
||||
hl_group = nil, -- Optional hl_group
|
||||
},
|
||||
---@class NoiceFormatOptions.title
|
||||
title = {
|
||||
hl_group = "NoiceFormatTitle",
|
||||
},
|
||||
---@class NoiceFormatOptions.event
|
||||
event = {
|
||||
hl_group = "NoiceFormatEvent",
|
||||
},
|
||||
---@class NoiceFormatOptions.kind
|
||||
kind = {
|
||||
hl_group = "NoiceFormatKind",
|
||||
},
|
||||
---@class NoiceFormatOptions.date
|
||||
date = {
|
||||
format = "%X", --- @see https://www.lua.org/pil/22.1.html
|
||||
hl_group = "NoiceFormatDate",
|
||||
},
|
||||
---@class NoiceFormatOptions.message
|
||||
message = {
|
||||
hl_group = nil, -- if set, then the hl_group will be used instead of the message highlights
|
||||
},
|
||||
---@class NoiceFormatOptions.confirm
|
||||
confirm = {
|
||||
hl_group = {
|
||||
choice = "NoiceFormatConfirm",
|
||||
default_choice = "NoiceFormatConfirmDefault",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return M
|
||||
@ -0,0 +1,145 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Util = require("noice.util")
|
||||
|
||||
-- Build docs with:
|
||||
-- lua require("noice.config.highlights").docs()
|
||||
|
||||
local M = {}
|
||||
|
||||
M.defaults = {
|
||||
Cmdline = "MsgArea", -- Normal for the classic cmdline area at the bottom"
|
||||
CmdlineIcon = "DiagnosticSignInfo", -- Cmdline icon
|
||||
CmdlineIconSearch = "DiagnosticSignWarn", -- Cmdline search icon (`/` and `?`)
|
||||
CmdlinePrompt = "Title", -- prompt for input()
|
||||
CmdlinePopup = "Normal", -- Normal for the cmdline popup
|
||||
CmdlinePopupBorder = "DiagnosticSignInfo", -- Cmdline popup border
|
||||
CmdlinePopupTitle = "DiagnosticSignInfo", -- Cmdline popup border
|
||||
CmdlinePopupBorderSearch = "DiagnosticSignWarn", -- Cmdline popup border for search
|
||||
Confirm = "Normal", -- Normal for the confirm view
|
||||
ConfirmBorder = "DiagnosticSignInfo", -- Border for the confirm view
|
||||
Cursor = "Cursor", -- Fake Cursor
|
||||
Mini = "MsgArea", -- Normal for mini view
|
||||
Popup = "NormalFloat", -- Normal for popup views
|
||||
PopupBorder = "FloatBorder", -- Border for popup views
|
||||
Popupmenu = "Pmenu", -- Normal for the popupmenu
|
||||
PopupmenuBorder = "FloatBorder", -- Popupmenu border
|
||||
PopupmenuMatch = "Special", -- Part of the item that matches the input
|
||||
PopupmenuSelected = "PmenuSel", -- Selected item in the popupmenu
|
||||
Scrollbar = "PmenuSbar", -- Normal for scrollbar
|
||||
ScrollbarThumb = "PmenuThumb", -- Scrollbar thumb
|
||||
Split = "NormalFloat", -- Normal for split views
|
||||
SplitBorder = "FloatBorder", -- Border for split views
|
||||
VirtualText = "DiagnosticVirtualTextInfo", -- Default hl group for virtualtext views
|
||||
FormatProgressDone = "Search", -- Progress bar done
|
||||
FormatProgressTodo = "CursorLine", -- progress bar todo
|
||||
FormatEvent = "NonText",
|
||||
FormatKind = "NonText",
|
||||
FormatDate = "Special",
|
||||
FormatConfirm = "CursorLine",
|
||||
FormatConfirmDefault = "Visual",
|
||||
FormatTitle = "Title",
|
||||
FormatLevelDebug = "NonText",
|
||||
FormatLevelTrace = "NonText",
|
||||
FormatLevelOff = "NonText",
|
||||
FormatLevelInfo = "DiagnosticVirtualTextInfo",
|
||||
FormatLevelWarn = "DiagnosticVirtualTextWarn",
|
||||
FormatLevelError = "DiagnosticVirtualTextError",
|
||||
LspProgressSpinner = "Constant", -- Lsp progress spinner
|
||||
LspProgressTitle = "NonText", -- Lsp progress title
|
||||
LspProgressClient = "Title", -- Lsp progress client name
|
||||
CompletionItemMenu = "none", -- Normal for the popupmenu
|
||||
CompletionItemWord = "none", -- Normal for the popupmenu
|
||||
CompletionItemKindDefault = "Special",
|
||||
CompletionItemKindColor = "NoiceCompletionItemKindDefault",
|
||||
CompletionItemKindFunction = "NoiceCompletionItemKindDefault",
|
||||
CompletionItemKindClass = "NoiceCompletionItemKindDefault",
|
||||
CompletionItemKindMethod = "NoiceCompletionItemKindDefault",
|
||||
CompletionItemKindConstructor = "NoiceCompletionItemKindDefault",
|
||||
CompletionItemKindInterface = "NoiceCompletionItemKindDefault",
|
||||
CompletionItemKindModule = "NoiceCompletionItemKindDefault",
|
||||
CompletionItemKindStruct = "NoiceCompletionItemKindDefault",
|
||||
CompletionItemKindKeyword = "NoiceCompletionItemKindDefault",
|
||||
CompletionItemKindValue = "NoiceCompletionItemKindDefault",
|
||||
CompletionItemKindProperty = "NoiceCompletionItemKindDefault",
|
||||
CompletionItemKindConstant = "NoiceCompletionItemKindDefault",
|
||||
CompletionItemKindSnippet = "NoiceCompletionItemKindDefault",
|
||||
CompletionItemKindFolder = "NoiceCompletionItemKindDefault",
|
||||
CompletionItemKindText = "NoiceCompletionItemKindDefault",
|
||||
CompletionItemKindEnumMember = "NoiceCompletionItemKindDefault",
|
||||
CompletionItemKindUnit = "NoiceCompletionItemKindDefault",
|
||||
CompletionItemKindField = "NoiceCompletionItemKindDefault",
|
||||
CompletionItemKindFile = "NoiceCompletionItemKindDefault",
|
||||
CompletionItemKindVariable = "NoiceCompletionItemKindDefault",
|
||||
CompletionItemKindEnum = "NoiceCompletionItemKindDefault",
|
||||
}
|
||||
|
||||
function M.add(hl_group, link)
|
||||
if not M.defaults[hl_group] then
|
||||
M.defaults[hl_group] = link
|
||||
end
|
||||
end
|
||||
|
||||
function M.get_link(hl_group)
|
||||
local ok, opts = pcall(vim.api.nvim_get_hl_by_name, hl_group, true)
|
||||
if opts then
|
||||
opts[vim.type_idx] = nil
|
||||
end
|
||||
if not ok or vim.tbl_isempty(opts) then
|
||||
opts = { link = hl_group }
|
||||
end
|
||||
opts.default = true
|
||||
return opts
|
||||
end
|
||||
|
||||
function M.setup()
|
||||
for hl, link in pairs(M.defaults) do
|
||||
if link ~= "none" then
|
||||
local opts = { link = link, default = true }
|
||||
|
||||
if vim.tbl_contains({ "IncSearch", "Search" }, link) then
|
||||
opts = M.get_link(link)
|
||||
end
|
||||
|
||||
vim.api.nvim_set_hl(0, "Noice" .. hl, opts)
|
||||
end
|
||||
end
|
||||
vim.api.nvim_set_hl(0, "NoiceHiddenCursor", { blend = 100, nocombine = true })
|
||||
end
|
||||
|
||||
function M.docs()
|
||||
local me = debug.getinfo(1, "S").source:sub(2)
|
||||
---@type table<string,string>
|
||||
local docs = {}
|
||||
local lines = io.open(me, "r"):lines()
|
||||
for line in lines do
|
||||
---@type string, string
|
||||
local hl, comment = line:match("%s*([a-zA-Z]+)%s*=.*%-%-%s*(.*)")
|
||||
if hl then
|
||||
docs[hl] = comment
|
||||
end
|
||||
end
|
||||
|
||||
local rows = {}
|
||||
table.insert(rows, { "Highlight Group", "Default Group", "Description" })
|
||||
table.insert(rows, { "---", "---", "---" })
|
||||
|
||||
Util.for_each(M.defaults, function(hl, link)
|
||||
table.insert(rows, { "**Noice" .. hl .. "**", "_" .. link .. "_", docs[hl] or "" })
|
||||
end)
|
||||
|
||||
local text = table.concat(
|
||||
vim.tbl_map(function(row)
|
||||
return "| " .. table.concat(row, " | ") .. " |"
|
||||
end, rows),
|
||||
"\n"
|
||||
)
|
||||
|
||||
text = "<!-- hl_start -->\n" .. text .. "\n<!-- hl_end -->"
|
||||
|
||||
local readme = Util.read_file("README.md")
|
||||
readme = readme:gsub("<%!%-%- hl_start %-%->.*<%!%-%- hl_end %-%->", text)
|
||||
Util.write_file("README.md", readme)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,28 @@
|
||||
local M = {}
|
||||
|
||||
---@class NoicePopupmenuItemKind
|
||||
M.kinds = {
|
||||
Class = " ",
|
||||
Color = " ",
|
||||
Constant = " ",
|
||||
Constructor = " ",
|
||||
Enum = "了 ",
|
||||
EnumMember = " ",
|
||||
Field = " ",
|
||||
File = " ",
|
||||
Folder = " ",
|
||||
Function = " ",
|
||||
Interface = " ",
|
||||
Keyword = " ",
|
||||
Method = "ƒ ",
|
||||
Module = " ",
|
||||
Property = " ",
|
||||
Snippet = " ",
|
||||
Struct = " ",
|
||||
Text = " ",
|
||||
Unit = " ",
|
||||
Value = " ",
|
||||
Variable = " ",
|
||||
}
|
||||
|
||||
return M
|
||||
@ -0,0 +1,301 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Routes = require("noice.config.routes")
|
||||
|
||||
local M = {}
|
||||
|
||||
M.ns = vim.api.nvim_create_namespace("noice")
|
||||
|
||||
function M.defaults()
|
||||
---@class NoiceConfig
|
||||
local defaults = {
|
||||
cmdline = {
|
||||
enabled = true, -- enables the Noice cmdline UI
|
||||
view = "cmdline_popup", -- view for rendering the cmdline. Change to `cmdline` to get a classic cmdline at the bottom
|
||||
opts = {}, -- global options for the cmdline. See section on views
|
||||
---@type table<string, CmdlineFormat>
|
||||
format = {
|
||||
-- conceal: (default=true) This will hide the text in the cmdline that matches the pattern.
|
||||
-- view: (default is cmdline view)
|
||||
-- opts: any options passed to the view
|
||||
-- icon_hl_group: optional hl_group for the icon
|
||||
-- title: set to anything or empty string to hide
|
||||
cmdline = { pattern = "^:", icon = "", lang = "vim" },
|
||||
search_down = { kind = "search", pattern = "^/", icon = " ", lang = "regex" },
|
||||
search_up = { kind = "search", pattern = "^%?", icon = " ", lang = "regex" },
|
||||
filter = { pattern = "^:%s*!", icon = "$", lang = "bash" },
|
||||
lua = { pattern = { "^:%s*lua%s+", "^:%s*lua%s*=%s*", "^:%s*=%s*" }, icon = "", lang = "lua" },
|
||||
help = { pattern = "^:%s*he?l?p?%s+", icon = "" },
|
||||
calculator = { pattern = "^=", icon = "", lang = "vimnormal" },
|
||||
input = {}, -- Used by input()
|
||||
-- lua = false, -- to disable a format, set to `false`
|
||||
},
|
||||
},
|
||||
messages = {
|
||||
-- NOTE: If you enable messages, then the cmdline is enabled automatically.
|
||||
-- This is a current Neovim limitation.
|
||||
enabled = true, -- enables the Noice messages UI
|
||||
view = "notify", -- default view for messages
|
||||
view_error = "notify", -- view for errors
|
||||
view_warn = "notify", -- view for warnings
|
||||
view_history = "messages", -- view for :messages
|
||||
view_search = "virtualtext", -- view for search count messages. Set to `false` to disable
|
||||
},
|
||||
popupmenu = {
|
||||
enabled = true, -- enables the Noice popupmenu UI
|
||||
---@type 'nui'|'cmp'
|
||||
backend = "nui", -- backend to use to show regular cmdline completions
|
||||
---@type NoicePopupmenuItemKind|false
|
||||
-- Icons for completion item kinds (see defaults at noice.config.icons.kinds)
|
||||
kind_icons = {}, -- set to `false` to disable icons
|
||||
},
|
||||
-- default options for require('noice').redirect
|
||||
-- see the section on Command Redirection
|
||||
---@type NoiceRouteConfig
|
||||
redirect = {
|
||||
view = "popup",
|
||||
filter = { event = "msg_show" },
|
||||
},
|
||||
-- You can add any custom commands below that will be available with `:Noice command`
|
||||
---@type table<string, NoiceCommand>
|
||||
commands = {
|
||||
history = {
|
||||
-- options for the message history that you get with `:Noice`
|
||||
view = "split",
|
||||
opts = { enter = true, format = "details" },
|
||||
filter = {
|
||||
any = {
|
||||
{ event = "notify" },
|
||||
{ error = true },
|
||||
{ warning = true },
|
||||
{ event = "msg_show", kind = { "" } },
|
||||
{ event = "lsp", kind = "message" },
|
||||
},
|
||||
},
|
||||
},
|
||||
-- :Noice last
|
||||
last = {
|
||||
view = "popup",
|
||||
opts = { enter = true, format = "details" },
|
||||
filter = {
|
||||
any = {
|
||||
{ event = "notify" },
|
||||
{ error = true },
|
||||
{ warning = true },
|
||||
{ event = "msg_show", kind = { "" } },
|
||||
{ event = "lsp", kind = "message" },
|
||||
},
|
||||
},
|
||||
filter_opts = { count = 1 },
|
||||
},
|
||||
-- :Noice errors
|
||||
errors = {
|
||||
-- options for the message history that you get with `:Noice`
|
||||
view = "popup",
|
||||
opts = { enter = true, format = "details" },
|
||||
filter = { error = true },
|
||||
filter_opts = { reverse = true },
|
||||
},
|
||||
all = {
|
||||
-- options for the message history that you get with `:Noice`
|
||||
view = "split",
|
||||
opts = { enter = true, format = "details" },
|
||||
filter = {},
|
||||
},
|
||||
},
|
||||
notify = {
|
||||
-- Noice can be used as `vim.notify` so you can route any notification like other messages
|
||||
-- Notification messages have their level and other properties set.
|
||||
-- event is always "notify" and kind can be any log level as a string
|
||||
-- The default routes will forward notifications to nvim-notify
|
||||
-- Benefit of using Noice for this is the routing and consistent history view
|
||||
enabled = true,
|
||||
view = "notify",
|
||||
},
|
||||
lsp = {
|
||||
progress = {
|
||||
enabled = true,
|
||||
-- Lsp Progress is formatted using the builtins for lsp_progress. See config.format.builtin
|
||||
-- See the section on formatting for more details on how to customize.
|
||||
--- @type NoiceFormat|string
|
||||
format = "lsp_progress",
|
||||
--- @type NoiceFormat|string
|
||||
format_done = "lsp_progress_done",
|
||||
throttle = 1000 / 10, -- frequency to update lsp progress message
|
||||
view = "mini",
|
||||
},
|
||||
override = {
|
||||
-- override the default lsp markdown formatter with Noice
|
||||
["vim.lsp.util.convert_input_to_markdown_lines"] = false,
|
||||
-- override the lsp markdown formatter with Noice
|
||||
["vim.lsp.util.stylize_markdown"] = false,
|
||||
-- override cmp documentation with Noice (needs the other options to work)
|
||||
["cmp.entry.get_documentation"] = false,
|
||||
},
|
||||
hover = {
|
||||
enabled = true,
|
||||
silent = false, -- set to true to not show a message if hover is not available
|
||||
view = nil, -- when nil, use defaults from documentation
|
||||
---@type NoiceViewOptions
|
||||
opts = {}, -- merged with defaults from documentation
|
||||
},
|
||||
signature = {
|
||||
enabled = true,
|
||||
auto_open = {
|
||||
enabled = true,
|
||||
trigger = true, -- Automatically show signature help when typing a trigger character from the LSP
|
||||
luasnip = true, -- Will open signature help when jumping to Luasnip insert nodes
|
||||
throttle = 50, -- Debounce lsp signature help request by 50ms
|
||||
},
|
||||
view = nil, -- when nil, use defaults from documentation
|
||||
---@type NoiceViewOptions
|
||||
opts = {}, -- merged with defaults from documentation
|
||||
},
|
||||
message = {
|
||||
-- Messages shown by lsp servers
|
||||
enabled = true,
|
||||
view = "notify",
|
||||
opts = {},
|
||||
},
|
||||
-- defaults for hover and signature help
|
||||
documentation = {
|
||||
view = "hover",
|
||||
---@type NoiceViewOptions
|
||||
opts = {
|
||||
replace = true,
|
||||
render = "plain",
|
||||
format = { "{message}" },
|
||||
win_options = { concealcursor = "n", conceallevel = 3 },
|
||||
},
|
||||
},
|
||||
},
|
||||
markdown = {
|
||||
hover = {
|
||||
["|(%S-)|"] = vim.cmd.help, -- vim help links
|
||||
["%[.-%]%((%S-)%)"] = require("noice.util").open, -- markdown links
|
||||
},
|
||||
highlights = {
|
||||
["|%S-|"] = "@text.reference",
|
||||
["@%S+"] = "@parameter",
|
||||
["^%s*(Parameters:)"] = "@text.title",
|
||||
["^%s*(Return:)"] = "@text.title",
|
||||
["^%s*(See also:)"] = "@text.title",
|
||||
["{%S-}"] = "@parameter",
|
||||
},
|
||||
},
|
||||
health = {
|
||||
checker = true, -- Disable if you don't want health checks to run
|
||||
},
|
||||
smart_move = {
|
||||
-- noice tries to move out of the way of existing floating windows.
|
||||
enabled = true, -- you can disable this behaviour here
|
||||
-- add any filetypes here, that shouldn't trigger smart move.
|
||||
excluded_filetypes = { "cmp_menu", "cmp_docs", "notify" },
|
||||
},
|
||||
---@type NoicePresets
|
||||
presets = {
|
||||
-- you can enable a preset by setting it to true, or a table that will override the preset config
|
||||
-- you can also add custom presets that you can enable/disable with enabled=true
|
||||
bottom_search = false, -- use a classic bottom cmdline for search
|
||||
command_palette = false, -- position the cmdline and popupmenu together
|
||||
long_message_to_split = false, -- long messages will be sent to a split
|
||||
inc_rename = false, -- enables an input dialog for inc-rename.nvim
|
||||
lsp_doc_border = false, -- add a border to hover docs and signature help
|
||||
cmdline_output_to_split = false, -- send the output of a command you executed in the cmdline to a split
|
||||
},
|
||||
throttle = 1000 / 30, -- how frequently does Noice need to check for ui updates? This has no effect when in blocking mode.
|
||||
---@type NoiceConfigViews
|
||||
views = {}, ---@see section on views
|
||||
---@type NoiceRouteConfig[]
|
||||
routes = {}, --- @see section on routes
|
||||
---@type table<string, NoiceFilter>
|
||||
status = {}, --- @see section on statusline components
|
||||
---@type NoiceFormatOptions
|
||||
format = {}, --- @see section on formatting
|
||||
debug = false,
|
||||
log = vim.fn.stdpath("state") .. "/noice.log",
|
||||
log_max_size = 1024 * 1024 * 2, -- 10MB
|
||||
}
|
||||
return defaults
|
||||
end
|
||||
|
||||
--- @type NoiceConfig
|
||||
M.options = {}
|
||||
|
||||
M._running = false
|
||||
function M.is_running()
|
||||
return M._running
|
||||
end
|
||||
|
||||
function M.setup(options)
|
||||
options = options or {}
|
||||
|
||||
M.fix_legacy(options)
|
||||
|
||||
if options.popupmenu and options.popupmenu.kind_icons == true then
|
||||
options.popupmenu.kind_icons = nil
|
||||
end
|
||||
|
||||
M.options = vim.tbl_deep_extend("force", {}, M.defaults(), {
|
||||
views = require("noice.config.views").defaults,
|
||||
status = require("noice.config.status").defaults,
|
||||
format = require("noice.config.format").defaults,
|
||||
popupmenu = {
|
||||
kind_icons = require("noice.config.icons").kinds,
|
||||
},
|
||||
})
|
||||
|
||||
M.truncate_log()
|
||||
|
||||
require("noice.config.preset").setup(options)
|
||||
|
||||
local routes = M.options.routes
|
||||
M.options = vim.tbl_deep_extend("force", M.options, options)
|
||||
vim.list_extend(M.options.routes, routes)
|
||||
|
||||
if M.options.popupmenu.kind_icons == false then
|
||||
M.options.popupmenu.kind_icons = {}
|
||||
end
|
||||
|
||||
require("noice.config.cmdline").setup()
|
||||
|
||||
M.options.routes = Routes.get(M.options.routes)
|
||||
|
||||
require("noice.config.highlights").setup()
|
||||
vim.api.nvim_create_autocmd("ColorScheme", {
|
||||
callback = function()
|
||||
require("noice.config.highlights").setup()
|
||||
end,
|
||||
})
|
||||
|
||||
require("noice.lsp").setup()
|
||||
M._running = true
|
||||
end
|
||||
|
||||
function M.truncate_log()
|
||||
local stat = vim.loop.fs_stat(M.options.log)
|
||||
if stat and stat.size > M.options.log_max_size then
|
||||
io.open(M.options.log, "w+"):close()
|
||||
end
|
||||
end
|
||||
|
||||
---@param opts NoiceConfig
|
||||
function M.fix_legacy(opts)
|
||||
if opts.lsp and opts.lsp.signature and type(opts.lsp.signature.auto_open) == "boolean" then
|
||||
opts.lsp.signature.auto_open = {
|
||||
enabled = opts.lsp.signature.auto_open,
|
||||
}
|
||||
end
|
||||
if opts.lsp_progress then
|
||||
opts.lsp = opts.lsp or {}
|
||||
opts.lsp.progress = opts.lsp_progress
|
||||
opts.lsp_progress = nil
|
||||
end
|
||||
if opts.history then
|
||||
opts.commands = opts.commands or {}
|
||||
opts.commands.history = opts.history
|
||||
opts.history = nil
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,133 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Util = require("noice.util")
|
||||
local Config = require("noice.config")
|
||||
|
||||
---@class NoicePreset: NoiceConfig
|
||||
---@field enabled? boolean
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.setup(options)
|
||||
for name, preset in pairs(options.presets or {}) do
|
||||
if preset ~= false then
|
||||
M.load(name, preset)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param name string
|
||||
---@param preset NoiceConfig|boolean
|
||||
function M.load(name, preset)
|
||||
if preset == true and not M.presets[name] then
|
||||
return Util.panic("Unknown preset " .. name)
|
||||
end
|
||||
|
||||
preset = vim.tbl_deep_extend("force", {}, M.presets[name] or {}, type(preset) == "table" and preset or {})
|
||||
---@cast preset NoicePreset
|
||||
|
||||
if preset.enabled == false then
|
||||
return
|
||||
end
|
||||
|
||||
local routes = preset.routes
|
||||
preset.routes = nil
|
||||
Config.options = vim.tbl_deep_extend("force", Config.options, preset)
|
||||
if routes then
|
||||
vim.list_extend(Config.options.routes, routes)
|
||||
end
|
||||
end
|
||||
|
||||
---@class NoicePresets: table<string, NoicePreset|boolean>
|
||||
M.presets = {
|
||||
bottom_search = {
|
||||
cmdline = {
|
||||
format = {
|
||||
search_down = {
|
||||
view = "cmdline",
|
||||
},
|
||||
search_up = {
|
||||
view = "cmdline",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
lsp_doc_border = {
|
||||
views = {
|
||||
hover = {
|
||||
border = {
|
||||
style = "rounded",
|
||||
},
|
||||
position = { row = 2, col = 2 },
|
||||
},
|
||||
},
|
||||
},
|
||||
command_palette = {
|
||||
views = {
|
||||
cmdline_popup = {
|
||||
position = {
|
||||
row = 3,
|
||||
col = "50%",
|
||||
},
|
||||
size = {
|
||||
min_width = 60,
|
||||
width = "auto",
|
||||
height = "auto",
|
||||
},
|
||||
},
|
||||
cmdline_popupmenu = {
|
||||
relative = "editor",
|
||||
position = {
|
||||
row = 6,
|
||||
col = "50%",
|
||||
},
|
||||
size = {
|
||||
width = 60,
|
||||
height = "auto",
|
||||
max_height = 15,
|
||||
},
|
||||
border = {
|
||||
style = "rounded",
|
||||
padding = { 0, 1 },
|
||||
},
|
||||
win_options = {
|
||||
winhighlight = { Normal = "Normal", FloatBorder = "NoiceCmdlinePopupBorder" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
long_message_to_split = {
|
||||
routes = {
|
||||
{
|
||||
filter = { event = "msg_show", min_height = 20 },
|
||||
view = "cmdline_output",
|
||||
},
|
||||
},
|
||||
},
|
||||
inc_rename = {
|
||||
cmdline = {
|
||||
format = {
|
||||
IncRename = {
|
||||
pattern = "^:%s*IncRename%s+",
|
||||
icon = " ",
|
||||
conceal = true,
|
||||
opts = {
|
||||
relative = "cursor",
|
||||
size = { min_width = 20 },
|
||||
position = { row = -3, col = 0 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
cmdline_output_to_split = {
|
||||
routes = {
|
||||
{
|
||||
view = "cmdline_output",
|
||||
filter = { cmdline = "^:" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return M
|
||||
@ -0,0 +1,125 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Config = require("noice.config")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@param routes? NoiceRouteConfig[]
|
||||
function M.get(routes)
|
||||
---@type NoiceRouteConfig[]
|
||||
local ret = {}
|
||||
|
||||
-- add custom routes
|
||||
vim.list_extend(ret, routes or {})
|
||||
|
||||
-- add default routes
|
||||
vim.list_extend(ret, M.defaults())
|
||||
return ret
|
||||
end
|
||||
|
||||
---@return NoiceRouteConfig[]
|
||||
function M.defaults()
|
||||
---@type NoiceRouteConfig[]
|
||||
local ret = {}
|
||||
|
||||
for _, kind in ipairs({ "signature", "hover" }) do
|
||||
table.insert(ret, {
|
||||
view = Config.options.lsp[kind].view or Config.options.lsp.documentation.view,
|
||||
filter = { event = "lsp", kind = kind },
|
||||
opts = vim.tbl_deep_extend(
|
||||
"force",
|
||||
{},
|
||||
Config.options.lsp.documentation.opts,
|
||||
Config.options.lsp[kind].opts or {}
|
||||
),
|
||||
})
|
||||
end
|
||||
|
||||
return vim.list_extend(ret, {
|
||||
{
|
||||
view = Config.options.cmdline.view,
|
||||
opts = Config.options.cmdline.opts,
|
||||
filter = { event = "cmdline" },
|
||||
},
|
||||
{
|
||||
view = "confirm",
|
||||
filter = {
|
||||
any = {
|
||||
{ event = "msg_show", kind = "confirm" },
|
||||
{ event = "msg_show", kind = "confirm_sub" },
|
||||
-- { event = "msg_show", kind = { "echo", "echomsg", "" }, before = true },
|
||||
-- { event = "msg_show", kind = { "echo", "echomsg" }, instant = true },
|
||||
-- { event = "msg_show", find = "E325" },
|
||||
-- { event = "msg_show", find = "Found a swap file" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
view = Config.options.messages.view_history,
|
||||
filter = {
|
||||
any = {
|
||||
{ event = "msg_history_show" },
|
||||
-- { min_height = 20 },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
view = Config.options.messages.view_search,
|
||||
filter = {
|
||||
event = "msg_show",
|
||||
kind = "search_count",
|
||||
},
|
||||
},
|
||||
{
|
||||
filter = {
|
||||
any = {
|
||||
{ event = { "msg_showmode", "msg_showcmd", "msg_ruler" } },
|
||||
{ event = "msg_show", kind = "search_count" },
|
||||
},
|
||||
},
|
||||
opts = { skip = true },
|
||||
},
|
||||
{
|
||||
view = Config.options.messages.view,
|
||||
filter = {
|
||||
event = "msg_show",
|
||||
kind = { "", "echo", "echomsg" },
|
||||
},
|
||||
opts = { replace = true, merge = true, title = "Messages" },
|
||||
},
|
||||
{
|
||||
view = Config.options.messages.view_error,
|
||||
filter = { error = true },
|
||||
opts = { title = "Error" },
|
||||
},
|
||||
{
|
||||
view = Config.options.messages.view_warn,
|
||||
filter = { warning = true },
|
||||
opts = { title = "Warning" },
|
||||
},
|
||||
{
|
||||
view = Config.options.notify.view,
|
||||
filter = { event = "notify" },
|
||||
opts = { title = "Notify" },
|
||||
},
|
||||
{
|
||||
view = Config.options.notify.view,
|
||||
filter = {
|
||||
event = "noice",
|
||||
kind = { "stats", "debug" },
|
||||
},
|
||||
opts = { lang = "lua", replace = true, title = "Noice" },
|
||||
},
|
||||
{
|
||||
view = Config.options.lsp.progress.view,
|
||||
filter = { event = "lsp", kind = "progress" },
|
||||
},
|
||||
{
|
||||
view = Config.options.lsp.message.view,
|
||||
opts = Config.options.lsp.message.opts,
|
||||
filter = { event = "lsp", kind = "message" },
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,16 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Msg = require("noice.ui.msg")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@type table<string, NoiceFilter>
|
||||
M.defaults = {
|
||||
ruler = { event = Msg.events.ruler },
|
||||
message = { event = Msg.events.show },
|
||||
command = { event = Msg.events.showcmd },
|
||||
mode = { event = Msg.events.showmode },
|
||||
search = { event = Msg.events.show, kind = Msg.kinds.search_count },
|
||||
}
|
||||
|
||||
return M
|
||||
@ -0,0 +1,259 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Util = require("noice.util")
|
||||
local Config = require("noice.config")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@param view string
|
||||
---@return NoiceViewOptions
|
||||
function M.get_options(view)
|
||||
if not view then
|
||||
Util.panic("View is missing?")
|
||||
end
|
||||
|
||||
local opts = { view = view }
|
||||
|
||||
local done = {}
|
||||
while opts.view and not done[opts.view] do
|
||||
done[opts.view] = true
|
||||
|
||||
local view_opts = vim.deepcopy(Config.options.views[opts.view] or {})
|
||||
opts = vim.tbl_deep_extend("keep", opts, view_opts)
|
||||
opts.view = view_opts.view
|
||||
end
|
||||
|
||||
return opts
|
||||
end
|
||||
|
||||
---@class NoiceConfigViews: table<string, NoiceViewOptions>
|
||||
M.defaults = {
|
||||
popupmenu = {
|
||||
relative = "editor",
|
||||
zindex = 65,
|
||||
position = "auto", -- when auto, then it will be positioned to the cmdline or cursor
|
||||
size = {
|
||||
width = "auto",
|
||||
height = "auto",
|
||||
max_height = 20,
|
||||
-- min_width = 10,
|
||||
},
|
||||
win_options = {
|
||||
winbar = "",
|
||||
foldenable = false,
|
||||
cursorline = true,
|
||||
cursorlineopt = "line",
|
||||
winhighlight = {
|
||||
Normal = "NoicePopupmenu", -- change to NormalFloat to make it look like other floats
|
||||
FloatBorder = "NoicePopupmenuBorder", -- border highlight
|
||||
CursorLine = "NoicePopupmenuSelected", -- used for highlighting the selected item
|
||||
PmenuMatch = "NoicePopupmenuMatch", -- used to highlight the part of the item that matches the input
|
||||
},
|
||||
},
|
||||
border = {
|
||||
padding = { 0, 1 },
|
||||
},
|
||||
},
|
||||
cmdline_popupmenu = {
|
||||
view = "popupmenu",
|
||||
zindex = 200,
|
||||
},
|
||||
virtualtext = {
|
||||
backend = "virtualtext",
|
||||
format = { "{message}" },
|
||||
hl_group = "NoiceVirtualText",
|
||||
},
|
||||
notify = {
|
||||
backend = "notify",
|
||||
fallback = "mini",
|
||||
format = "notify",
|
||||
replace = false,
|
||||
merge = false,
|
||||
},
|
||||
split = {
|
||||
backend = "split",
|
||||
enter = false,
|
||||
relative = "editor",
|
||||
position = "bottom",
|
||||
size = "20%",
|
||||
close = {
|
||||
keys = { "q" },
|
||||
},
|
||||
win_options = {
|
||||
winhighlight = { Normal = "NoiceSplit", FloatBorder = "NoiceSplitBorder" },
|
||||
wrap = true,
|
||||
},
|
||||
},
|
||||
cmdline_output = {
|
||||
format = "details",
|
||||
view = "split",
|
||||
},
|
||||
messages = {
|
||||
view = "split",
|
||||
enter = true,
|
||||
},
|
||||
vsplit = {
|
||||
view = "split",
|
||||
position = "right",
|
||||
},
|
||||
popup = {
|
||||
backend = "popup",
|
||||
relative = "editor",
|
||||
close = {
|
||||
events = { "BufLeave" },
|
||||
keys = { "q" },
|
||||
},
|
||||
enter = true,
|
||||
border = {
|
||||
style = "rounded",
|
||||
},
|
||||
position = "50%",
|
||||
size = {
|
||||
width = "120",
|
||||
height = "20",
|
||||
},
|
||||
win_options = {
|
||||
winhighlight = { Normal = "NoicePopup", FloatBorder = "NoicePopupBorder" },
|
||||
winbar = "",
|
||||
foldenable = false,
|
||||
},
|
||||
},
|
||||
hover = {
|
||||
view = "popup",
|
||||
relative = "cursor",
|
||||
zindex = 45,
|
||||
enter = false,
|
||||
anchor = "auto",
|
||||
size = {
|
||||
width = "auto",
|
||||
height = "auto",
|
||||
max_height = 20,
|
||||
max_width = 120,
|
||||
},
|
||||
border = {
|
||||
style = "none",
|
||||
padding = { 0, 2 },
|
||||
},
|
||||
position = { row = 1, col = 0 },
|
||||
win_options = {
|
||||
wrap = true,
|
||||
linebreak = true,
|
||||
},
|
||||
},
|
||||
cmdline = {
|
||||
backend = "popup",
|
||||
relative = "editor",
|
||||
position = {
|
||||
row = "100%",
|
||||
col = 0,
|
||||
},
|
||||
size = {
|
||||
height = "auto",
|
||||
width = "100%",
|
||||
},
|
||||
border = {
|
||||
style = "none",
|
||||
},
|
||||
win_options = {
|
||||
winhighlight = {
|
||||
Normal = "NoiceCmdline",
|
||||
IncSearch = "",
|
||||
CurSearch = "",
|
||||
Search = "",
|
||||
},
|
||||
},
|
||||
},
|
||||
mini = {
|
||||
backend = "mini",
|
||||
relative = "editor",
|
||||
align = "message-right",
|
||||
timeout = 2000,
|
||||
reverse = true,
|
||||
focusable = false,
|
||||
position = {
|
||||
row = -1,
|
||||
col = "100%",
|
||||
-- col = 0,
|
||||
},
|
||||
size = "auto",
|
||||
border = {
|
||||
style = "none",
|
||||
},
|
||||
zindex = 60,
|
||||
win_options = {
|
||||
winbar = "",
|
||||
foldenable = false,
|
||||
winblend = 30,
|
||||
winhighlight = {
|
||||
Normal = "NoiceMini",
|
||||
IncSearch = "",
|
||||
CurSearch = "",
|
||||
Search = "",
|
||||
},
|
||||
},
|
||||
},
|
||||
cmdline_popup = {
|
||||
backend = "popup",
|
||||
relative = "editor",
|
||||
focusable = false,
|
||||
enter = false,
|
||||
zindex = 200,
|
||||
position = {
|
||||
row = "50%",
|
||||
col = "50%",
|
||||
},
|
||||
size = {
|
||||
min_width = 60,
|
||||
width = "auto",
|
||||
height = "auto",
|
||||
},
|
||||
border = {
|
||||
style = "rounded",
|
||||
padding = { 0, 1 },
|
||||
},
|
||||
win_options = {
|
||||
winhighlight = {
|
||||
Normal = "NoiceCmdlinePopup",
|
||||
FloatTitle = "NoiceCmdlinePopupTitle",
|
||||
FloatBorder = "NoiceCmdlinePopupBorder",
|
||||
IncSearch = "",
|
||||
CurSearch = "",
|
||||
Search = "",
|
||||
},
|
||||
winbar = "",
|
||||
foldenable = false,
|
||||
cursorline = false,
|
||||
},
|
||||
},
|
||||
confirm = {
|
||||
backend = "popup",
|
||||
relative = "editor",
|
||||
focusable = false,
|
||||
align = "center",
|
||||
enter = false,
|
||||
zindex = 210,
|
||||
format = { "{confirm}" },
|
||||
position = {
|
||||
row = "50%",
|
||||
col = "50%",
|
||||
},
|
||||
size = "auto",
|
||||
border = {
|
||||
style = "rounded",
|
||||
padding = { 0, 1 },
|
||||
text = {
|
||||
top = " Confirm ",
|
||||
},
|
||||
},
|
||||
win_options = {
|
||||
winhighlight = {
|
||||
Normal = "NoiceConfirm",
|
||||
FloatBorder = "NoiceConfirmBorder",
|
||||
},
|
||||
winbar = "",
|
||||
foldenable = false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return M
|
||||
242
config/neovim/store/lazy-plugins/noice.nvim/lua/noice/health.lua
Normal file
242
config/neovim/store/lazy-plugins/noice.nvim/lua/noice/health.lua
Normal file
@ -0,0 +1,242 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Util = require("noice.util")
|
||||
local Config = require("noice.config")
|
||||
local Lsp = require("noice.lsp")
|
||||
local Treesitter = require("noice.text.treesitter")
|
||||
|
||||
local start = vim.health.start or vim.health.report_start
|
||||
local ok = vim.health.ok or vim.health.report_ok
|
||||
local warn = vim.health.warn or vim.health.report_warn
|
||||
local error = vim.health.error or vim.health.report_error
|
||||
|
||||
local M = {}
|
||||
|
||||
M.checks = {}
|
||||
|
||||
M.log = {
|
||||
---@class NoiceHealthLog
|
||||
checkhealth = {
|
||||
start = function(msg)
|
||||
start(msg or "noice.nvim")
|
||||
end,
|
||||
info = function(msg, ...)
|
||||
info(msg:format(...))
|
||||
end,
|
||||
ok = function(msg, ...)
|
||||
ok(msg:format(...))
|
||||
end,
|
||||
warn = function(msg, ...)
|
||||
warn(msg:format(...))
|
||||
end,
|
||||
error = function(msg, ...)
|
||||
error(msg:format(...))
|
||||
end,
|
||||
},
|
||||
---@type NoiceHealthLog
|
||||
notify = {
|
||||
start = function(msg) end,
|
||||
info = function(msg, ...)
|
||||
Util.info(msg:format(...))
|
||||
end,
|
||||
ok = function(msg, ...) end,
|
||||
warn = function(msg, ...)
|
||||
Util.warn_once(msg:format(...))
|
||||
end,
|
||||
error = function(msg, ...)
|
||||
Util.error_once(msg:format(...))
|
||||
end,
|
||||
},
|
||||
}
|
||||
|
||||
---@param opts? {checkhealth?: boolean}
|
||||
function M.check(opts)
|
||||
opts = opts or {}
|
||||
opts.checkhealth = opts.checkhealth == nil and true or opts.checkhealth
|
||||
|
||||
local log = opts.checkhealth and M.log.checkhealth or M.log.notify
|
||||
|
||||
log.start()
|
||||
|
||||
if vim.fn.has("nvim-0.9.0") ~= 1 then
|
||||
log.error("Noice requires Neovim >= 0.9.0")
|
||||
-- require("noice.util").error("Noice needs Neovim >= 0.9.0 (nightly)")
|
||||
if not opts.checkhealth then
|
||||
return
|
||||
end
|
||||
else
|
||||
log.ok("**Neovim** >= 0.9.0")
|
||||
if opts.checkhealth and vim.fn.has("nvim-0.10.0") ~= 1 then
|
||||
log.warn("**Neovim** >= 0.10 is highly recommended, since it fixes some issues related to `vim.ui_attach`")
|
||||
end
|
||||
end
|
||||
|
||||
local uis = vim.api.nvim_list_uis()
|
||||
for _, ui in ipairs(uis) do
|
||||
local ok = true
|
||||
for _, ext in ipairs({ "ext_cmdline", "ext_popupmenu", "ext_messages" }) do
|
||||
if ui[ext] then
|
||||
ok = false
|
||||
log.error(
|
||||
"You're using a GUI that uses " .. ext .. ". Noice can't work when the GUI has " .. ext .. " enabled."
|
||||
)
|
||||
end
|
||||
end
|
||||
if ok then
|
||||
if ui.chan == 0 then
|
||||
log.ok("You're not using a GUI")
|
||||
else
|
||||
log.ok("You're using a GUI that should work ok")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if vim.go.lazyredraw then
|
||||
if not Config.is_running() then
|
||||
log.warn(
|
||||
"You have enabled 'lazyredraw' (see `:h 'lazyredraw'`)\nThis is only meant to be set temporarily.\nYou'll experience issues using Noice."
|
||||
)
|
||||
end
|
||||
else
|
||||
log.ok("**vim.go.lazyredraw** is not enabled")
|
||||
end
|
||||
|
||||
if opts.checkhealth then
|
||||
if not Util.module_exists("notify") then
|
||||
log.warn("Noice needs nvim-notify for routes using the `notify` view")
|
||||
if not opts.checkhealth then
|
||||
return
|
||||
end
|
||||
else
|
||||
log.ok("**nvim-notify** is installed")
|
||||
end
|
||||
|
||||
if vim.o.shortmess:find("S") then
|
||||
log.warn(
|
||||
"You added `S` to `vim.opt.shortmess`. Search count messages will not be handled by Noice. So no virtual text for search count."
|
||||
)
|
||||
end
|
||||
|
||||
for _, lang in ipairs({ "vim", "regex", "lua", "bash", "markdown", "markdown_inline" }) do
|
||||
if Treesitter.has_lang(lang) then
|
||||
log.ok("**TreeSitter " .. lang .. "** parser is installed")
|
||||
else
|
||||
log.warn(
|
||||
"**TreeSitter "
|
||||
.. lang
|
||||
.. "** parser is not installed. Highlighting of the cmdline for "
|
||||
.. lang
|
||||
.. " might be broken"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if Config.is_running() then
|
||||
---@type {opt:string[], opt_str?:string, handler:fun(), handler_str:string}
|
||||
local checks = {
|
||||
{
|
||||
opt = "notify",
|
||||
enabled = Config.options.notify.enabled,
|
||||
handler = vim.notify,
|
||||
handler_str = "vim.notify",
|
||||
},
|
||||
{
|
||||
opt = "lsp.hover",
|
||||
enabled = Config.options.lsp.hover.enabled,
|
||||
handler = vim.lsp.handlers["textDocument/hover"],
|
||||
handler_str = 'vim.lsp.handlers["textDocument/hover"]',
|
||||
},
|
||||
{
|
||||
opt = "lsp.signature",
|
||||
enabled = Config.options.lsp.signature.enabled,
|
||||
handler = vim.lsp.handlers["textDocument/signatureHelp"],
|
||||
handler_str = 'vim.lsp.handlers["textDocument/signatureHelp"]',
|
||||
},
|
||||
{
|
||||
opt = "lsp.message",
|
||||
enabled = Config.options.lsp.message.enabled,
|
||||
handler = vim.lsp.handlers["window/showMessage"],
|
||||
handler_str = 'vim.lsp.handlers["window/showMessage"]',
|
||||
},
|
||||
{
|
||||
opt = 'lsp.override["vim.lsp.util.convert_input_to_markdown_lines"]',
|
||||
enabled = Config.options.lsp.override["vim.lsp.util.convert_input_to_markdown_lines"],
|
||||
handler = vim.lsp.util.convert_input_to_markdown_lines,
|
||||
handler_str = "vim.lsp.util.convert_input_to_markdown_lines",
|
||||
},
|
||||
{
|
||||
opt = 'lsp.override["vim.lsp.util.stylize_markdown"]',
|
||||
enabled = Config.options.lsp.override["vim.lsp.util.stylize_markdown"],
|
||||
handler = vim.lsp.util.stylize_markdown,
|
||||
handler_str = "vim.lsp.util.stylize_markdown",
|
||||
},
|
||||
}
|
||||
|
||||
if package.loaded["cmp.entry"] then
|
||||
local mod = package.loaded["cmp.entry"]
|
||||
table.insert(checks, {
|
||||
opt = 'lsp.override["cmp.entry.get_documentation"]',
|
||||
enabled = Config.options.lsp.override["cmp.entry.get_documentation"],
|
||||
handler = mod.get_documentation,
|
||||
handler_str = "cmp.entry.get_documentation",
|
||||
})
|
||||
end
|
||||
|
||||
for _, check in ipairs(checks) do
|
||||
if check.handler then
|
||||
if check.enabled then
|
||||
local source = M.get_source(check.handler)
|
||||
if source.plugin ~= "noice.nvim" then
|
||||
log.error(([[`%s` has been overwritten by another plugin?
|
||||
|
||||
Either disable the other plugin or set `config.%s.enabled = false` in your **Noice** config.
|
||||
- plugin: %s
|
||||
- file: %s
|
||||
- line: %s]]):format(check.handler_str, check.opt, source.plugin, source.source, source.line))
|
||||
else
|
||||
log.ok(("`%s` is set to **Noice**"):format(check.handler_str))
|
||||
end
|
||||
elseif opts.checkhealth then
|
||||
log.warn("`" .. check.handler_str .. "` is not configured to be handled by **Noice**")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function M.get_source(fn)
|
||||
local info = debug.getinfo(fn, "S")
|
||||
local source = info.source:sub(2)
|
||||
---@class FunSource
|
||||
local ret = {
|
||||
line = info.linedefined,
|
||||
source = source,
|
||||
plugin = "unknown",
|
||||
}
|
||||
if source:find("noice") then
|
||||
ret.plugin = "noice.nvim"
|
||||
elseif source:find("/runtime/lua/") then
|
||||
ret.plugin = "nvim"
|
||||
else
|
||||
local opt = source:match("/pack/[^%/]-/opt/([^%/]-)/")
|
||||
local start = source:match("/pack/[^%/]-/start/([^%/]-)/")
|
||||
ret.plugin = opt or start or "unknown"
|
||||
end
|
||||
return ret
|
||||
end
|
||||
M.check({ checkhealth = false })
|
||||
|
||||
M.checker = Util.interval(1000, function()
|
||||
if Config.is_running() then
|
||||
M.check({ checkhealth = false })
|
||||
end
|
||||
end, {
|
||||
enabled = function()
|
||||
return Config.is_running()
|
||||
end,
|
||||
})
|
||||
|
||||
return M
|
||||
@ -0,0 +1,83 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Health = require("noice.health")
|
||||
local Api = require("noice.api")
|
||||
local Config = require("noice.config")
|
||||
|
||||
local M = {}
|
||||
|
||||
M.api = Api
|
||||
|
||||
---@param opts? NoiceConfig
|
||||
function M.setup(opts)
|
||||
-- run some checks before setting up
|
||||
if not Health.check({ checkhealth = false, loaded = false }) then
|
||||
return
|
||||
end
|
||||
|
||||
local function load()
|
||||
require("noice.util").try(function()
|
||||
require("noice.config").setup(opts)
|
||||
require("noice.commands").setup()
|
||||
require("noice.message.router").setup()
|
||||
M.enable()
|
||||
end)
|
||||
end
|
||||
|
||||
if vim.v.vim_did_enter == 0 then
|
||||
-- Schedule loading after VimEnter. Get the UI up and running first.
|
||||
vim.api.nvim_create_autocmd("VimEnter", {
|
||||
once = true,
|
||||
callback = load,
|
||||
})
|
||||
else
|
||||
-- Schedule on the event loop
|
||||
vim.schedule(load)
|
||||
end
|
||||
end
|
||||
|
||||
function M.disable()
|
||||
Config._running = false
|
||||
if Config.options.notify.enabled then
|
||||
require("noice.source.notify").disable()
|
||||
end
|
||||
require("noice.message.router").disable()
|
||||
require("noice.ui").disable()
|
||||
require("noice.util.hacks").disable()
|
||||
end
|
||||
|
||||
M.deactivate = M.disable
|
||||
|
||||
function M.cmd(name)
|
||||
require("noice.commands").cmd(name)
|
||||
end
|
||||
|
||||
function M.enable()
|
||||
Config._running = true
|
||||
if Config.options.notify.enabled then
|
||||
require("noice.source.notify").enable()
|
||||
end
|
||||
require("noice.util.hacks").enable()
|
||||
require("noice.ui").enable()
|
||||
require("noice.message.router").enable()
|
||||
|
||||
if Config.options.health.checker then
|
||||
Health.checker()
|
||||
end
|
||||
end
|
||||
|
||||
-- Redirect any messages generated by a command or function
|
||||
---@param cmd string|fun() command or function to execute
|
||||
---@param routes? NoiceRouteConfig[] custom routes. Defaults to `config.redirect`
|
||||
function M.redirect(cmd, routes)
|
||||
return require("noice.message.router").redirect(cmd, routes)
|
||||
end
|
||||
|
||||
---@param msg string
|
||||
---@param level number|string
|
||||
---@param opts? table<string, any>
|
||||
function M.notify(msg, level, opts)
|
||||
return require("noice.source.notify").notify(msg, level, opts)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,82 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Manager = require("noice.message.manager")
|
||||
local Util = require("noice.util")
|
||||
local Message = require("noice.message")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@type table<LspKind, NoiceMessage>
|
||||
M._messages = {}
|
||||
|
||||
---@type number?
|
||||
M._autohide = nil
|
||||
|
||||
function M.autohide()
|
||||
if not M._autohide then
|
||||
M._autohide = vim.api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI", "InsertEnter" }, {
|
||||
group = vim.api.nvim_create_augroup("noice_lsp_docs", { clear = true }),
|
||||
callback = function()
|
||||
vim.defer_fn(M.on_close, 10)
|
||||
end,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
---@param kind LspKind
|
||||
function M.get(kind)
|
||||
if not M._messages[kind] then
|
||||
M._messages[kind] = Message("lsp", kind)
|
||||
M._messages[kind].opts.title = kind
|
||||
end
|
||||
M._messages[kind]:clear()
|
||||
return M._messages[kind]
|
||||
end
|
||||
|
||||
function M.on_close()
|
||||
for _, message in pairs(M._messages) do
|
||||
-- close the message if we're not in it's buffer (focus)
|
||||
local keep = message:on_buf(vim.api.nvim_get_current_buf()) or (message.opts.stay and message.opts.stay())
|
||||
if not keep then
|
||||
M.hide(message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.scroll(delta)
|
||||
for _, kind in ipairs({ "hover", "signature" }) do
|
||||
local message = M.get(kind)
|
||||
local win = message:win()
|
||||
if win then
|
||||
Util.nui.scroll(win, delta)
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param message NoiceMessage
|
||||
function M.hide(message)
|
||||
message.opts.keep = function()
|
||||
return false
|
||||
end
|
||||
Manager.remove(message)
|
||||
end
|
||||
|
||||
---@param message NoiceMessage
|
||||
---@param stay? fun():boolean
|
||||
function M.show(message, stay)
|
||||
M.autohide()
|
||||
message.opts.timeout = 100
|
||||
message.opts.keep = function()
|
||||
return true
|
||||
end
|
||||
message.opts.stay = stay
|
||||
for _, m in pairs(M._messages) do
|
||||
if m ~= message then
|
||||
M.hide(m)
|
||||
end
|
||||
end
|
||||
Manager.add(message)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,53 @@
|
||||
local require = require("noice.util.lazy")
|
||||
local Util = require("noice.util")
|
||||
|
||||
local Markdown = require("noice.text.markdown")
|
||||
|
||||
---@alias MarkedString string | { language: string; value: string }
|
||||
---@alias MarkupContent { kind: ('plaintext' | 'markdown'), value: string}
|
||||
---@alias MarkupContents MarkedString | MarkedString[] | MarkupContent
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Formats the content and adds it to the message
|
||||
---@param contents MarkupContents Markup content
|
||||
function M.format_markdown(contents)
|
||||
if type(contents) ~= "table" or not Util.islist(contents) then
|
||||
contents = { contents }
|
||||
end
|
||||
|
||||
local parts = {}
|
||||
|
||||
for _, content in ipairs(contents) do
|
||||
if type(content) == "string" then
|
||||
table.insert(parts, content)
|
||||
elseif content.language then
|
||||
table.insert(parts, ("```%s\n%s\n```"):format(content.language, content.value))
|
||||
elseif content.kind == "markdown" then
|
||||
table.insert(parts, content.value)
|
||||
elseif content.kind == "plaintext" then
|
||||
table.insert(parts, ("```\n%s\n```"):format(content.value))
|
||||
elseif Util.islist(content) then
|
||||
vim.list_extend(parts, M.format_markdown(content))
|
||||
elseif type(content) == "table" and next(content) == nil then
|
||||
goto continue
|
||||
else
|
||||
error("Unknown markup " .. vim.inspect(content))
|
||||
end
|
||||
::continue::
|
||||
end
|
||||
|
||||
return vim.split(table.concat(parts, "\n"), "\n")
|
||||
end
|
||||
|
||||
-- Formats the content and adds it to the message
|
||||
---@param contents MarkupContents Markup content
|
||||
---@param message NoiceMessage Noice message
|
||||
---@param opts? MarkdownFormatOptions
|
||||
function M.format(message, contents, opts)
|
||||
local text = table.concat(M.format_markdown(contents), "\n")
|
||||
Markdown.format(message, text, opts)
|
||||
return message
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,37 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Format = require("noice.lsp.format")
|
||||
local Util = require("noice.util")
|
||||
local Docs = require("noice.lsp.docs")
|
||||
local Config = require("noice.config")
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.setup()
|
||||
vim.lsp.handlers["textDocument/hover"] = M.on_hover
|
||||
end
|
||||
|
||||
function M.on_hover(_, result, ctx)
|
||||
if not (result and result.contents) then
|
||||
if Config.options.lsp.hover.silent ~= true then
|
||||
vim.notify("No information available")
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local message = Docs.get("hover")
|
||||
|
||||
if not message:focus() then
|
||||
Format.format(message, result.contents, { ft = vim.bo[ctx.bufnr].filetype })
|
||||
if message:is_empty() then
|
||||
if Config.options.lsp.hover.silent ~= true then
|
||||
vim.notify("No information available")
|
||||
end
|
||||
return
|
||||
end
|
||||
Docs.show(message)
|
||||
end
|
||||
end
|
||||
M.on_hover = Util.protect(M.on_hover)
|
||||
|
||||
return M
|
||||
@ -0,0 +1,64 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Config = require("noice.config")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@alias LspEvent "lsp"
|
||||
M.event = "lsp"
|
||||
|
||||
---@enum LspKind
|
||||
M.kinds = {
|
||||
progress = "progress",
|
||||
hover = "hover",
|
||||
message = "message",
|
||||
signature = "signature",
|
||||
}
|
||||
|
||||
function M.setup()
|
||||
if Config.options.lsp.hover.enabled then
|
||||
require("noice.lsp.hover").setup()
|
||||
end
|
||||
|
||||
if Config.options.lsp.signature.enabled then
|
||||
require("noice.lsp.signature").setup()
|
||||
end
|
||||
|
||||
if Config.options.lsp.message.enabled then
|
||||
require("noice.lsp.message").setup()
|
||||
end
|
||||
|
||||
if Config.options.lsp.progress.enabled then
|
||||
require("noice.lsp.progress").setup()
|
||||
end
|
||||
|
||||
local overrides = vim.tbl_filter(
|
||||
---@param v boolean
|
||||
function(v)
|
||||
return v
|
||||
end,
|
||||
Config.options.lsp.override
|
||||
)
|
||||
|
||||
if #overrides > 0 then
|
||||
require("noice.lsp.override").setup()
|
||||
end
|
||||
end
|
||||
|
||||
function M.scroll(delta)
|
||||
return require("noice.lsp.docs").scroll(delta)
|
||||
end
|
||||
|
||||
function M.hover()
|
||||
---@diagnostic disable-next-line: missing-parameter
|
||||
local params = vim.lsp.util.make_position_params()
|
||||
vim.lsp.buf_request(0, "textDocument/hover", params, require("noice.lsp.hover").on_hover)
|
||||
end
|
||||
|
||||
function M.signature()
|
||||
---@diagnostic disable-next-line: missing-parameter
|
||||
local params = vim.lsp.util.make_position_params()
|
||||
vim.lsp.buf_request(0, "textDocument/signatureHelp", params, require("noice.lsp.signature").on_signature)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,40 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Manager = require("noice.message.manager")
|
||||
local Message = require("noice.message")
|
||||
local Util = require("noice.util")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@enum MessageType
|
||||
M.message_type = {
|
||||
error = 1,
|
||||
warn = 2,
|
||||
info = 3,
|
||||
debug = 4,
|
||||
}
|
||||
|
||||
---@alias ShowMessageParams {type:MessageType, message:string}
|
||||
|
||||
function M.setup()
|
||||
vim.lsp.handlers["window/showMessage"] = Util.protect(M.on_message)
|
||||
end
|
||||
|
||||
---@param result ShowMessageParams
|
||||
function M.on_message(_, result, ctx)
|
||||
---@type number
|
||||
local client_id = ctx.client_id
|
||||
local client = vim.lsp.get_client_by_id(client_id)
|
||||
local client_name = client and client.name or string.format("lsp id=%d", client_id)
|
||||
|
||||
local message = Message("lsp", "message", result.message)
|
||||
message.opts.title = "LSP Message (" .. client_name .. ")"
|
||||
for level, type in pairs(M.message_type) do
|
||||
if type == result.type then
|
||||
message.level = level
|
||||
end
|
||||
end
|
||||
Manager.add(message)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,59 @@
|
||||
local require = require("noice.util.lazy")
|
||||
local Markdown = require("noice.text.markdown")
|
||||
|
||||
local Config = require("noice.config")
|
||||
local Format = require("noice.lsp.format")
|
||||
local Message = require("noice.message")
|
||||
local Hacks = require("noice.util.hacks")
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.setup()
|
||||
if Config.options.lsp.override["cmp.entry.get_documentation"] then
|
||||
Hacks.on_module("cmp.entry", function(mod)
|
||||
mod.get_documentation = function(self)
|
||||
local item = self:get_completion_item()
|
||||
|
||||
local lines = item.documentation and Format.format_markdown(item.documentation) or {}
|
||||
local ret = table.concat(lines, "\n")
|
||||
local detail = item.detail
|
||||
if detail and type(detail) == "table" then
|
||||
detail = table.concat(detail, "\n")
|
||||
end
|
||||
|
||||
if detail and not ret:find(detail, 1, true) then
|
||||
local ft = self.context.filetype
|
||||
local dot_index = string.find(ft, "%.")
|
||||
if dot_index ~= nil then
|
||||
ft = string.sub(ft, 0, dot_index - 1)
|
||||
end
|
||||
ret = ("```%s\n%s\n```\n%s"):format(ft, vim.trim(detail), ret)
|
||||
end
|
||||
return vim.split(ret, "\n")
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
if Config.options.lsp.override["vim.lsp.util.convert_input_to_markdown_lines"] then
|
||||
vim.lsp.util.convert_input_to_markdown_lines = function(input, contents)
|
||||
contents = contents or {}
|
||||
local ret = Format.format_markdown(input)
|
||||
vim.list_extend(contents, ret)
|
||||
return contents
|
||||
end
|
||||
end
|
||||
|
||||
if Config.options.lsp.override["vim.lsp.util.stylize_markdown"] then
|
||||
vim.lsp.util.stylize_markdown = function(buf, contents, _opts)
|
||||
vim.api.nvim_buf_clear_namespace(buf, Config.ns, 0, -1)
|
||||
local text = table.concat(contents, "\n")
|
||||
local message = Message("lsp")
|
||||
Markdown.format(message, text)
|
||||
message:render(buf, Config.ns)
|
||||
Markdown.keys(buf)
|
||||
return vim.api.nvim_buf_get_lines(buf, 0, -1, false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,113 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Message = require("noice.message")
|
||||
local Manager = require("noice.message.manager")
|
||||
local Router = require("noice.message.router")
|
||||
local Format = require("noice.text.format")
|
||||
local Config = require("noice.config")
|
||||
local Util = require("noice.util")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@type table<string, NoiceMessage>
|
||||
M._progress = {}
|
||||
M._running = false
|
||||
|
||||
---@param data {client_id: integer, params: lsp.ProgressParams}
|
||||
function M.progress(data)
|
||||
local client_id = data.client_id
|
||||
local params = data.params or data.result -- TODO: Remove data.result after nvim 0.10 release
|
||||
local id = client_id .. "." .. params.token
|
||||
|
||||
local message = M._progress[id]
|
||||
if not message then
|
||||
local client = vim.lsp.get_client_by_id(client_id)
|
||||
-- should not happen, but it does for some reason
|
||||
if not client then
|
||||
return
|
||||
end
|
||||
message = Message("lsp", "progress")
|
||||
message.opts.progress = {
|
||||
client_id = client_id,
|
||||
---@type string
|
||||
client = client and client.name or ("lsp-" .. client_id),
|
||||
}
|
||||
M._progress[id] = message
|
||||
end
|
||||
|
||||
message.opts.progress = vim.tbl_deep_extend("force", message.opts.progress, params.value)
|
||||
message.opts.progress.id = id
|
||||
|
||||
if params.value.kind == "end" then
|
||||
if message.opts.progress.percentage then
|
||||
message.opts.progress.percentage = 100
|
||||
end
|
||||
vim.defer_fn(function()
|
||||
M.close(id)
|
||||
end, 100)
|
||||
end
|
||||
|
||||
M.update()
|
||||
end
|
||||
|
||||
function M.close(id)
|
||||
local message = M._progress[id]
|
||||
if message then
|
||||
M.update()
|
||||
Router.update()
|
||||
Manager.remove(message)
|
||||
M._progress[id] = nil
|
||||
end
|
||||
end
|
||||
|
||||
function M._update()
|
||||
if not vim.tbl_isempty(M._progress) then
|
||||
for id, message in pairs(M._progress) do
|
||||
local client = vim.lsp.get_client_by_id(message.opts.progress.client_id)
|
||||
if not client then
|
||||
M.close(id)
|
||||
end
|
||||
if message.opts.progress.kind == "end" then
|
||||
Manager.add(Format.format(message, Config.options.lsp.progress.format_done))
|
||||
else
|
||||
Manager.add(Format.format(message, Config.options.lsp.progress.format))
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
function M.update()
|
||||
error("should never be called")
|
||||
end
|
||||
|
||||
function M.setup()
|
||||
M.update = Util.interval(Config.options.lsp.progress.throttle, M._update, {
|
||||
enabled = function()
|
||||
return not vim.tbl_isempty(M._progress)
|
||||
end,
|
||||
})
|
||||
|
||||
-- Neovim >= 0.10.0
|
||||
local ok = pcall(vim.api.nvim_create_autocmd, "LspProgress", {
|
||||
group = vim.api.nvim_create_augroup("noice_lsp_progress", { clear = true }),
|
||||
callback = function(event)
|
||||
M.progress(event.data)
|
||||
end,
|
||||
})
|
||||
|
||||
-- Neovim < 0.10.0
|
||||
if not ok then
|
||||
local orig = vim.lsp.handlers["$/progress"]
|
||||
vim.lsp.handlers["$/progress"] = function(...)
|
||||
local params = select(2, ...)
|
||||
local ctx = select(3, ...)
|
||||
Util.try(function()
|
||||
M.progress({ client_id = ctx.client_id, params = params })
|
||||
end)
|
||||
orig(...)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,247 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local NoiceText = require("noice.text")
|
||||
local Format = require("noice.lsp.format")
|
||||
local Markdown = require("noice.text.markdown")
|
||||
local Config = require("noice.config")
|
||||
local Util = require("noice.util")
|
||||
local Docs = require("noice.lsp.docs")
|
||||
|
||||
---@class SignatureInformation
|
||||
---@field label string
|
||||
---@field documentation? string|MarkupContent
|
||||
---@field parameters? ParameterInformation[]
|
||||
---@field activeParameter? integer
|
||||
|
||||
---@class ParameterInformation
|
||||
---@field label string|{[1]:integer, [2]:integer}
|
||||
---@field documentation? string|MarkupContent
|
||||
|
||||
---@class SignatureHelpContext
|
||||
---@field triggerKind SignatureHelpTriggerKind
|
||||
---@field triggerCharacter? string
|
||||
---@field isRetrigger boolean
|
||||
---@field activeSignatureHelp? SignatureHelp
|
||||
|
||||
---@class SignatureHelp
|
||||
---@field signatures SignatureInformation[]
|
||||
---@field activeSignature? integer
|
||||
---@field activeParameter? integer
|
||||
---@field ft? string
|
||||
---@field message NoiceMessage
|
||||
local M = {}
|
||||
M.__index = M
|
||||
|
||||
---@enum SignatureHelpTriggerKind
|
||||
M.trigger_kind = {
|
||||
invoked = 1,
|
||||
trigger_character = 2,
|
||||
content_change = 3,
|
||||
}
|
||||
|
||||
function M.setup()
|
||||
vim.lsp.handlers["textDocument/signatureHelp"] = M.on_signature
|
||||
|
||||
if Config.options.lsp.signature.auto_open.enabled then
|
||||
-- attach to existing buffers
|
||||
for _, client in ipairs((vim.lsp.get_clients or vim.lsp.get_active_clients)()) do
|
||||
for _, buf in ipairs(vim.lsp.get_buffers_by_client_id(client.id)) do
|
||||
M.on_attach(buf, client)
|
||||
end
|
||||
end
|
||||
|
||||
-- attach to new buffers
|
||||
vim.api.nvim_create_autocmd("LspAttach", {
|
||||
group = vim.api.nvim_create_augroup("noice_lsp_signature", { clear = true }),
|
||||
callback = function(args)
|
||||
if args.data ~= nil then
|
||||
M.on_attach(args.buf, vim.lsp.get_client_by_id(args.data.client_id))
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function M.get_char(buf)
|
||||
local current_win = vim.api.nvim_get_current_win()
|
||||
local win = buf == vim.api.nvim_win_get_buf(current_win) and current_win or vim.fn.bufwinid(buf)
|
||||
local cursor = vim.api.nvim_win_get_cursor(win == -1 and 0 or win)
|
||||
local row = cursor[1] - 1
|
||||
local col = cursor[2]
|
||||
local _, lines = pcall(vim.api.nvim_buf_get_text, buf, row, 0, row, col, {})
|
||||
local line = vim.trim(lines and lines[1] or "")
|
||||
return line:sub(-1, -1)
|
||||
end
|
||||
|
||||
---@param result SignatureHelp
|
||||
function M.on_signature(_, result, ctx, config)
|
||||
config = config or {}
|
||||
if not (result and result.signatures) then
|
||||
if not config.trigger then
|
||||
vim.notify("No signature help available")
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local message = Docs.get("signature")
|
||||
|
||||
if config.trigger or not message:focus() then
|
||||
result.ft = vim.bo[ctx.bufnr].filetype
|
||||
result.message = message
|
||||
M.new(result):format()
|
||||
if message:is_empty() then
|
||||
if not config.trigger then
|
||||
vim.notify("No signature help available")
|
||||
end
|
||||
return
|
||||
end
|
||||
Docs.show(message, config.stay)
|
||||
end
|
||||
end
|
||||
M.on_signature = Util.protect(M.on_signature)
|
||||
|
||||
function M.on_attach(buf, client)
|
||||
if client.server_capabilities.signatureHelpProvider then
|
||||
---@type string[]
|
||||
local chars = client.server_capabilities.signatureHelpProvider.triggerCharacters
|
||||
if chars and #chars > 0 then
|
||||
local callback = M.check(buf, chars, client.offset_encoding)
|
||||
if Config.options.lsp.signature.auto_open.luasnip then
|
||||
vim.api.nvim_create_autocmd("User", {
|
||||
pattern = "LuasnipInsertNodeEnter",
|
||||
callback = callback,
|
||||
})
|
||||
end
|
||||
if Config.options.lsp.signature.auto_open.trigger then
|
||||
vim.api.nvim_create_autocmd({ "TextChangedI", "TextChangedP", "InsertEnter" }, {
|
||||
buffer = buf,
|
||||
callback = callback,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.check(buf, chars, encoding)
|
||||
encoding = encoding or "utf-16"
|
||||
return Util.debounce(Config.options.lsp.signature.auto_open.throttle, function(_event)
|
||||
if vim.api.nvim_get_current_buf() ~= buf then
|
||||
return
|
||||
end
|
||||
|
||||
if vim.tbl_contains(chars, M.get_char(buf)) then
|
||||
local params = vim.lsp.util.make_position_params(0, encoding)
|
||||
vim.lsp.buf_request(buf, "textDocument/signatureHelp", params, function(err, result, ctx)
|
||||
M.on_signature(err, result, ctx, {
|
||||
trigger = true,
|
||||
stay = function()
|
||||
return vim.tbl_contains(chars, M.get_char(buf))
|
||||
end,
|
||||
})
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---@param help SignatureHelp
|
||||
function M.new(help)
|
||||
return setmetatable(help, M)
|
||||
end
|
||||
|
||||
function M:active_parameter(sig_index)
|
||||
if self.activeSignature and self.signatures[self.activeSignature + 1] and sig_index ~= self.activeSignature + 1 then
|
||||
return
|
||||
end
|
||||
local sig = self.signatures[sig_index]
|
||||
if sig.activeParameter and sig.parameters and sig.parameters[sig.activeParameter + 1] then
|
||||
return sig.parameters[sig.activeParameter + 1]
|
||||
end
|
||||
if self.activeParameter and sig.parameters and sig.parameters[self.activeParameter + 1] then
|
||||
return sig.parameters[self.activeParameter + 1]
|
||||
end
|
||||
return sig.parameters and sig.parameters[1] or nil
|
||||
end
|
||||
|
||||
---@param sig SignatureInformation
|
||||
---@param param ParameterInformation
|
||||
function M:format_active_parameter(sig, param)
|
||||
local label = param.label
|
||||
if type(label) == "string" then
|
||||
local from = sig.label:find(label, 1, true)
|
||||
if from then
|
||||
self.message:append(NoiceText("", {
|
||||
hl_group = "LspSignatureActiveParameter",
|
||||
col = from - 1,
|
||||
length = vim.fn.strlen(label),
|
||||
}))
|
||||
end
|
||||
else
|
||||
self.message:append(NoiceText("", {
|
||||
hl_group = "LspSignatureActiveParameter",
|
||||
col = label[1],
|
||||
length = label[2] - label[1],
|
||||
}))
|
||||
end
|
||||
end
|
||||
|
||||
--- dddd
|
||||
-- function M:format_signature(boo) end
|
||||
|
||||
---@param sig SignatureInformation
|
||||
---@overload fun() # goooo
|
||||
function M:format_signature(sig_index, sig)
|
||||
if sig_index ~= 1 then
|
||||
self.message:newline()
|
||||
self.message:newline()
|
||||
Markdown.horizontal_line(self.message)
|
||||
self.message:newline()
|
||||
end
|
||||
|
||||
local count = self.message:height()
|
||||
self.message:append(sig.label)
|
||||
self.message:append(NoiceText.syntax(self.ft, self.message:height() - count))
|
||||
local param = self:active_parameter(sig_index)
|
||||
if param then
|
||||
self:format_active_parameter(sig, param)
|
||||
end
|
||||
self.message:newline()
|
||||
|
||||
if sig.documentation then
|
||||
Markdown.horizontal_line(self.message)
|
||||
Format.format(self.message, sig.documentation, { ft = self.ft })
|
||||
end
|
||||
|
||||
---@type ParameterInformation[]
|
||||
local params = vim.tbl_filter(function(p)
|
||||
return p.documentation
|
||||
end, sig.parameters or {})
|
||||
|
||||
local lines = {}
|
||||
if #params > 0 then
|
||||
for _, p in ipairs(sig.parameters) do
|
||||
if p.documentation then
|
||||
local pdoc = table.concat(Format.format_markdown(p.documentation or ""), "\n")
|
||||
local line = { "-" }
|
||||
if p.label then
|
||||
local label = p.label
|
||||
if type(label) == "table" then
|
||||
label = sig.label:sub(label[1] + 1, label[2])
|
||||
end
|
||||
|
||||
line[#line + 1] = "`[" .. label .. "]`"
|
||||
end
|
||||
line[#line + 1] = pdoc
|
||||
lines[#lines + 1] = table.concat(line, " ")
|
||||
end
|
||||
end
|
||||
end
|
||||
Format.format(self.message, table.concat(lines, "\n"), { ft = self.ft })
|
||||
end
|
||||
|
||||
function M:format()
|
||||
for s, sig in ipairs(self.signatures) do
|
||||
self:format_signature(s, sig)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,183 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Util = require("noice.util")
|
||||
local Manager = require("noice.message.manager")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@alias NoiceFilterFun fun(message: NoiceMessage, ...): boolean
|
||||
|
||||
---@class NoiceFilter
|
||||
---@field any? NoiceFilter[]
|
||||
---@field blocking? boolean
|
||||
---@field cleared? boolean
|
||||
---@field cmdline? string|boolean
|
||||
---@field error? boolean
|
||||
---@field event? NoiceEvent|NoiceEvent[]
|
||||
---@field find? string
|
||||
---@field has? boolean
|
||||
---@field kind? NoiceKind|NoiceKind[]
|
||||
---@field max_height? integer
|
||||
---@field max_length? integer
|
||||
---@field max_width? integer
|
||||
---@field message? NoiceMessage|NoiceMessage[]
|
||||
---@field min_height? integer
|
||||
---@field min_length? integer
|
||||
---@field min_width? integer
|
||||
---@field mode? string
|
||||
---@field not? NoiceFilter
|
||||
---@field warning? boolean
|
||||
---@field cond? fun(message:NoiceMessage):boolean
|
||||
|
||||
-----@type table<string, NoiceFilterFun>
|
||||
M.filters = {
|
||||
cleared = function(message, cleared)
|
||||
---@cast message NoiceMessage
|
||||
return cleared == not Manager.has(message)
|
||||
end,
|
||||
has = function(message, has)
|
||||
---@cast message NoiceMessage
|
||||
return has == Manager.has(message, { history = true })
|
||||
end,
|
||||
cond = function(message, cond)
|
||||
return cond(message)
|
||||
end,
|
||||
mode = function(_, mode)
|
||||
return vim.api.nvim_get_mode().mode:find(mode)
|
||||
end,
|
||||
blocking = function(_, blocking)
|
||||
return blocking == Util.is_blocking()
|
||||
end,
|
||||
event = function(message, event)
|
||||
---@cast message NoiceMessage
|
||||
event = type(event) == "table" and event or { event }
|
||||
return vim.tbl_contains(event, message.event)
|
||||
end,
|
||||
kind = function(message, kind)
|
||||
---@cast message NoiceMessage
|
||||
kind = type(kind) == "table" and kind or { kind }
|
||||
return vim.tbl_contains(kind, message.kind)
|
||||
end,
|
||||
cmdline = function(message, cmdline)
|
||||
---@cast message NoiceMessage
|
||||
---@cast cmdline string|boolean
|
||||
if type(cmdline) == "boolean" then
|
||||
return (message.cmdline ~= nil) == cmdline
|
||||
end
|
||||
if message.cmdline then
|
||||
local str = message.cmdline.state.firstc .. message.cmdline:get()
|
||||
return str:find(cmdline)
|
||||
end
|
||||
return false
|
||||
end,
|
||||
message = function(message, other)
|
||||
---@cast message NoiceMessage
|
||||
other = Util.islist(other) and other or { other }
|
||||
---@cast other NoiceMessage[]
|
||||
for _, m in ipairs(other) do
|
||||
if m.id == message.id then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end,
|
||||
error = function(message, error)
|
||||
---@cast message NoiceMessage
|
||||
return error == (message.level == "error")
|
||||
end,
|
||||
warning = function(message, warning)
|
||||
---@cast message NoiceMessage
|
||||
return warning == (message.level == "warn")
|
||||
end,
|
||||
find = function(message, find)
|
||||
---@cast message NoiceMessage
|
||||
return message:content():find(find)
|
||||
end,
|
||||
min_height = function(message, min_height)
|
||||
---@cast message NoiceMessage
|
||||
return message:height() >= min_height
|
||||
end,
|
||||
max_height = function(message, max_height)
|
||||
---@cast message NoiceMessage
|
||||
return message:height() <= max_height
|
||||
end,
|
||||
min_width = function(message, min_width)
|
||||
---@cast message NoiceMessage
|
||||
return message:width() >= min_width
|
||||
end,
|
||||
max_width = function(message, max_width)
|
||||
---@cast message NoiceMessage
|
||||
return message:width() <= max_width
|
||||
end,
|
||||
min_length = function(message, min_length)
|
||||
---@cast message NoiceMessage
|
||||
return message:length() >= min_length
|
||||
end,
|
||||
max_length = function(message, max_length)
|
||||
---@cast message NoiceMessage
|
||||
return message:length() <= max_length
|
||||
end,
|
||||
any = function(message, any)
|
||||
---@cast message NoiceMessage
|
||||
---@cast any NoiceFilter[]
|
||||
for _, f in ipairs(any) do
|
||||
if message:is(f) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end,
|
||||
["not"] = function(message, filter)
|
||||
---@cast message NoiceMessage
|
||||
return not message:is(filter)
|
||||
end,
|
||||
}
|
||||
|
||||
---@type table<string,boolean>
|
||||
M._unknown_notified = {}
|
||||
|
||||
---@param message NoiceMessage
|
||||
---@param filter NoiceFilter
|
||||
function M.is(message, filter)
|
||||
for k, v in pairs(filter) do
|
||||
if M.filters[k] then
|
||||
if not M.filters[k](message, v) then
|
||||
return false
|
||||
end
|
||||
else
|
||||
if not M._unknown_notified[k] then
|
||||
M._unknown_notified[k] = true
|
||||
Util.error("Unknown filter key " .. k .. " for " .. vim.inspect(filter))
|
||||
end
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
---@param messages NoiceMessage[]
|
||||
---@param filter NoiceFilter
|
||||
---@param invert? boolean
|
||||
---@return NoiceMessage[]
|
||||
function M.filter(messages, filter, invert)
|
||||
return vim.tbl_filter(
|
||||
---@param message NoiceMessage
|
||||
function(message)
|
||||
local is = M.is(message, filter)
|
||||
if invert then
|
||||
is = not is
|
||||
end
|
||||
return is
|
||||
end,
|
||||
messages
|
||||
)
|
||||
end
|
||||
|
||||
---@param messages NoiceMessage[]
|
||||
---@param filter NoiceFilter
|
||||
---@param invert? boolean
|
||||
function M.has(messages, filter, invert)
|
||||
return #M.filter(messages, filter, invert) > 0
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,116 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Block = require("noice.text.block")
|
||||
local Filter = require("noice.message.filter")
|
||||
|
||||
local _id = 0
|
||||
|
||||
---@class NoiceMessage: NoiceBlock
|
||||
---@field super NoiceBlock
|
||||
---@field id number
|
||||
---@field event NoiceEvent
|
||||
---@field ctime number
|
||||
---@field mtime number
|
||||
---@field tick number
|
||||
---@field level? NotifyLevel
|
||||
---@field kind? NoiceKind
|
||||
---@field cmdline? NoiceCmdline
|
||||
---@field _debug? boolean
|
||||
---@field opts table<string, any>
|
||||
---@overload fun(event: NoiceEvent, kind?: NoiceKind, content?: NoiceContent|NoiceContent[]): NoiceMessage
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local Message = Block:extend("NoiceBlock")
|
||||
|
||||
---@param event NoiceEvent
|
||||
---@param kind? NoiceKind
|
||||
---@param content? NoiceContent|NoiceContent[]
|
||||
function Message:init(event, kind, content)
|
||||
_id = _id + 1
|
||||
self.id = _id
|
||||
self.tick = 1
|
||||
self.ctime = vim.fn.localtime()
|
||||
self.mtime = vim.fn.localtime()
|
||||
self.event = event
|
||||
self.kind = kind
|
||||
self.opts = {}
|
||||
Message.super.init(self, content)
|
||||
end
|
||||
|
||||
-- Returns the first buffer that has rendered the message
|
||||
---@return buffer?
|
||||
function Message:buf()
|
||||
return self:bufs()[1]
|
||||
end
|
||||
|
||||
function Message:bufs()
|
||||
return vim.tbl_filter(function(buf)
|
||||
return vim.api.nvim_buf_is_valid(buf) and self.on_buf(buf)
|
||||
end, vim.api.nvim_list_bufs())
|
||||
end
|
||||
|
||||
function Message:wins()
|
||||
return vim.tbl_filter(function(win)
|
||||
return vim.api.nvim_win_is_valid(win) and self:on_win(win)
|
||||
end, vim.api.nvim_list_wins())
|
||||
end
|
||||
|
||||
-- Returns the first window that displays the message
|
||||
---@return window?
|
||||
function Message:win()
|
||||
return self:wins()[1]
|
||||
end
|
||||
|
||||
function Message:focus()
|
||||
local win = self:win()
|
||||
if win then
|
||||
vim.api.nvim_set_current_win(win)
|
||||
-- switch to normal mode
|
||||
vim.cmd("stopinsert")
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function Message:on_remove()
|
||||
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
|
||||
if self:on_buf(buf) then
|
||||
vim.b[buf].messages = vim.tbl_filter(function(b)
|
||||
return b ~= buf
|
||||
end, vim.b[buf].messages)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Message:on_win(win)
|
||||
return self:on_buf(vim.api.nvim_win_get_buf(win))
|
||||
end
|
||||
|
||||
function Message:on_buf(buf)
|
||||
return vim.b[buf].messages and vim.tbl_contains(vim.b[buf].messages, self.id)
|
||||
end
|
||||
|
||||
function Message:_add_buf(buf)
|
||||
local bufs = vim.b[buf].messages or {}
|
||||
table.insert(bufs, self.id)
|
||||
vim.b[buf].messages = bufs
|
||||
end
|
||||
|
||||
---@param bufnr number buffer number
|
||||
---@param ns_id number namespace id
|
||||
---@param linenr_start? number line number (1-indexed)
|
||||
function Message:highlight(bufnr, ns_id, linenr_start)
|
||||
self:_add_buf(bufnr)
|
||||
return Message.super.highlight(self, bufnr, ns_id, linenr_start)
|
||||
end
|
||||
|
||||
---@param bufnr number buffer number
|
||||
---@param ns_id number namespace id
|
||||
---@param linenr_start? number start line number (1-indexed)
|
||||
---@param linenr_end? number end line number (1-indexed)
|
||||
function Message:render(bufnr, ns_id, linenr_start, linenr_end)
|
||||
self:_add_buf(bufnr)
|
||||
return Message.super.render(self, bufnr, ns_id, linenr_start, linenr_end)
|
||||
end
|
||||
|
||||
Message.is = Filter.is
|
||||
|
||||
return Message
|
||||
@ -0,0 +1,129 @@
|
||||
local M = {}
|
||||
|
||||
local _tick = 1
|
||||
|
||||
local function next_tick()
|
||||
_tick = _tick + 1
|
||||
return _tick
|
||||
end
|
||||
|
||||
---@type table<number, NoiceMessage>
|
||||
M._history = {}
|
||||
|
||||
---@type table<number, NoiceMessage>
|
||||
M._messages = {}
|
||||
|
||||
function M.tick()
|
||||
return _tick
|
||||
end
|
||||
|
||||
---@param message NoiceMessage
|
||||
function M.add(message)
|
||||
if not (message:is_empty() and vim.tbl_isempty(message.opts)) then
|
||||
message.tick = next_tick()
|
||||
message.mtime = vim.fn.localtime()
|
||||
M._history[message.id] = message
|
||||
M._messages[message.id] = message
|
||||
end
|
||||
end
|
||||
|
||||
---@param message NoiceMessage
|
||||
---@param opts? { history: boolean } # defaults to `{ history = false }`
|
||||
function M.has(message, opts)
|
||||
opts = opts or {}
|
||||
return (opts.history and M._history or M._messages)[message.id] ~= nil
|
||||
end
|
||||
|
||||
---@param message NoiceMessage
|
||||
function M.remove(message)
|
||||
if M._history[message.id] then
|
||||
M._history[message.id] = nil
|
||||
next_tick()
|
||||
end
|
||||
if M._messages[message.id] then
|
||||
M._messages[message.id] = nil
|
||||
next_tick()
|
||||
end
|
||||
message:on_remove()
|
||||
end
|
||||
|
||||
---@param filter? NoiceFilter
|
||||
function M.clear(filter)
|
||||
M.with(function(message)
|
||||
M._messages[message.id] = nil
|
||||
next_tick()
|
||||
end, filter)
|
||||
end
|
||||
|
||||
---@param max number
|
||||
function M.prune(max)
|
||||
local keep = M.get(nil, { count = max })
|
||||
M._messages = {}
|
||||
for _, message in ipairs(keep) do
|
||||
M._messages[message.id] = message
|
||||
end
|
||||
end
|
||||
|
||||
-- Sorts messages in-place by mtime & id
|
||||
---@param messages NoiceMessage[]
|
||||
function M.sort(messages, reverse)
|
||||
table.sort(
|
||||
messages,
|
||||
---@param a NoiceMessage
|
||||
---@param b NoiceMessage
|
||||
function(a, b)
|
||||
local ret = (a.mtime == b.mtime) and (a.id < b.id) or (a.mtime < b.mtime)
|
||||
if reverse then
|
||||
ret = not ret
|
||||
end
|
||||
return ret
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
function M.get_by_id(id)
|
||||
return M._history[id]
|
||||
end
|
||||
|
||||
---@class NoiceMessageOpts
|
||||
---@field history? boolean
|
||||
---@field sort? boolean
|
||||
---@field reverse? boolean
|
||||
---@field count? number
|
||||
---@field messages? NoiceMessage[]
|
||||
|
||||
---@param filter? NoiceFilter
|
||||
---@param opts? NoiceMessageOpts
|
||||
---@return NoiceMessage[]
|
||||
function M.get(filter, opts)
|
||||
opts = opts or {}
|
||||
local messages = opts.messages or opts.history and M._history or M._messages
|
||||
local ret = {}
|
||||
for _, message in pairs(messages) do
|
||||
if not filter or message:is(filter) then
|
||||
table.insert(ret, message)
|
||||
end
|
||||
end
|
||||
if opts.sort then
|
||||
M.sort(ret, opts.reverse)
|
||||
end
|
||||
if opts.count and #ret > opts.count then
|
||||
local last = {}
|
||||
for i = #ret - opts.count + 1, #ret do
|
||||
table.insert(last, ret[i])
|
||||
end
|
||||
ret = last
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
---@param fn fun(message: NoiceMessage)
|
||||
---@param filter? NoiceFilter
|
||||
---@param opts? { history: boolean, sort: boolean } # defaults to `{ history = false, sort = false }`
|
||||
function M.with(fn, filter, opts)
|
||||
for _, message in ipairs(M.get(filter, opts)) do
|
||||
fn(message)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,260 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Config = require("noice.config")
|
||||
local Util = require("noice.util")
|
||||
local View = require("noice.view")
|
||||
local Manager = require("noice.message.manager")
|
||||
|
||||
---@class NoiceRoute
|
||||
---@field view NoiceView
|
||||
---@field filter NoiceFilter
|
||||
---@field opts? NoiceRouteOptions|NoiceViewOptions
|
||||
|
||||
---@class NoiceRouteOptions
|
||||
---@field stop boolean
|
||||
---@field skip boolean
|
||||
|
||||
---@class NoiceRouteConfig
|
||||
---@field view string
|
||||
---@field filter NoiceFilter
|
||||
---@field opts? NoiceRouteOptions|NoiceViewOptions
|
||||
|
||||
local M = {}
|
||||
---@type NoiceRoute[]
|
||||
M._routes = {}
|
||||
M._tick = 0
|
||||
M._need_redraw = false
|
||||
---@type fun()|Interval?
|
||||
M._updater = nil
|
||||
M._updating = false
|
||||
|
||||
function M.enable()
|
||||
if not M._updater then
|
||||
M._updater = Util.interval(Config.options.throttle, Util.protect(M.update))
|
||||
end
|
||||
M._updater()
|
||||
end
|
||||
|
||||
function M.disable()
|
||||
if M._updater then
|
||||
M._updater.stop()
|
||||
M._updater = nil
|
||||
Manager.clear()
|
||||
M.update()
|
||||
end
|
||||
vim.api.nvim_create_augroup("NoiceRouter", { clear = true })
|
||||
end
|
||||
|
||||
---@param route NoiceRouteConfig
|
||||
---@param pos? number
|
||||
function M.add(route, pos)
|
||||
local ret = {
|
||||
filter = route.filter,
|
||||
opts = route.opts or {},
|
||||
view = route.view and View.get_view(route.view, route.opts) or nil,
|
||||
}
|
||||
if ret.view == nil then
|
||||
ret.opts.skip = true
|
||||
end
|
||||
if pos then
|
||||
table.insert(M._routes, pos, ret)
|
||||
else
|
||||
table.insert(M._routes, ret)
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
-- Redirect any messages generated by a command or function
|
||||
---@param cmd string|fun() command or function to execute
|
||||
---@param routes? NoiceRouteConfig[] custom routes. Defaults to `config.redirect`
|
||||
function M.redirect(cmd, routes)
|
||||
routes = routes or { Config.options.redirect }
|
||||
if type(cmd) == "string" then
|
||||
local cmd_str = cmd
|
||||
cmd = function()
|
||||
vim.cmd(cmd_str)
|
||||
end
|
||||
end
|
||||
-- process any pending messages
|
||||
M.update()
|
||||
|
||||
local added = {}
|
||||
local pos = 1
|
||||
-- add temporary routes
|
||||
for _, route in ipairs(routes) do
|
||||
table.insert(added, M.add(route, pos))
|
||||
pos = pos + 1
|
||||
end
|
||||
|
||||
-- execute callback
|
||||
Util.try(cmd)
|
||||
|
||||
-- force a redraw to make sure we received all msg_show events
|
||||
vim.cmd.redraw()
|
||||
|
||||
-- process messages
|
||||
M.update()
|
||||
|
||||
-- remove temporary routes
|
||||
M._routes = vim.tbl_filter(function(r)
|
||||
return not vim.tbl_contains(added, r)
|
||||
end, M._routes)
|
||||
end
|
||||
|
||||
function M.setup()
|
||||
for _, route in ipairs(Config.options.routes) do
|
||||
M.add(route)
|
||||
end
|
||||
end
|
||||
|
||||
function M.check_redraw()
|
||||
if Util.is_blocking() and M._need_redraw then
|
||||
-- NOTE: set to false before actually calling redraw to prevent a loop with ui
|
||||
M._need_redraw = false
|
||||
Util.redraw()
|
||||
end
|
||||
end
|
||||
|
||||
function M.view_stats()
|
||||
local views = M.get_views()
|
||||
---@type table<string,number>
|
||||
local ret = {}
|
||||
|
||||
-- remove deleted messages and new messages from the views
|
||||
for view, _ in pairs(views) do
|
||||
if #view._messages > 0 then
|
||||
if not ret[view._opts.view] then
|
||||
ret[view._opts.view] = 0
|
||||
end
|
||||
ret[view._opts.view] = ret[view._opts.view] + #view._messages
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
function M.get_views()
|
||||
---@type table<NoiceView, boolean>
|
||||
local views = {}
|
||||
for _, route in ipairs(M._routes) do
|
||||
if route.view then
|
||||
views[route.view] = true
|
||||
end
|
||||
end
|
||||
return views
|
||||
end
|
||||
|
||||
function M.dismiss()
|
||||
Manager.clear()
|
||||
local views = M.get_views()
|
||||
for view, _ in pairs(views) do
|
||||
view:dismiss()
|
||||
view:display()
|
||||
end
|
||||
M.update()
|
||||
end
|
||||
|
||||
function M.update()
|
||||
if Util.is_exiting() then
|
||||
return
|
||||
end
|
||||
|
||||
if M._updating then
|
||||
return
|
||||
end
|
||||
|
||||
-- only update on changes
|
||||
if M._tick == Manager.tick() then
|
||||
M.check_redraw()
|
||||
return
|
||||
end
|
||||
|
||||
M._updating = true
|
||||
|
||||
Util.stats.track("router.update")
|
||||
|
||||
---@type table<NoiceView,boolean>
|
||||
local updates = {}
|
||||
|
||||
local messages = Manager.get(nil, { sort = true })
|
||||
local views = M.get_views()
|
||||
|
||||
-- remove deleted messages and new messages from the views
|
||||
for view, _ in pairs(views) do
|
||||
local count = #view._messages
|
||||
view._messages = Manager.get({
|
||||
-- remove any deleted messages
|
||||
has = true,
|
||||
-- remove messages that we are adding
|
||||
["not"] = {
|
||||
message = messages,
|
||||
},
|
||||
}, { messages = view._messages })
|
||||
|
||||
-- retry errors only once
|
||||
if view._errors > 1 then
|
||||
view._errors = 0
|
||||
end
|
||||
|
||||
if #view._messages ~= count or view._errors > 0 then
|
||||
updates[view] = true
|
||||
end
|
||||
end
|
||||
|
||||
-- add messages
|
||||
for _, message in ipairs(messages) do
|
||||
for _, route in ipairs(M._routes) do
|
||||
if message:is(route.filter) then
|
||||
if not route.opts.skip then
|
||||
route.view:push(message)
|
||||
route.view._route_opts = vim.tbl_deep_extend("force", route.view._route_opts or {}, route.opts or {})
|
||||
updates[route.view] = true
|
||||
end
|
||||
if route.opts.stop ~= false then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Manager.clear()
|
||||
|
||||
local dirty = false
|
||||
for view, _ in pairs(updates) do
|
||||
view:display()
|
||||
if view._errors > 0 then
|
||||
dirty = true
|
||||
end
|
||||
end
|
||||
|
||||
if not dirty then
|
||||
M._tick = Manager.tick()
|
||||
end
|
||||
|
||||
if not vim.tbl_isempty(updates) then
|
||||
Util.stats.track("router.update.updated")
|
||||
M._need_redraw = true
|
||||
end
|
||||
|
||||
M.check_redraw()
|
||||
M._updating = false
|
||||
end
|
||||
|
||||
function M.echo_pending()
|
||||
local messages = Manager.get({ event = "msg_show" }, { sort = true })
|
||||
local chunks = {}
|
||||
for _, message in ipairs(messages) do
|
||||
for _, line in ipairs(message._lines) do
|
||||
---@param t NuiText
|
||||
local chunk = vim.tbl_map(function(t)
|
||||
return { t:content(), t.extmark.hl_group }
|
||||
end, line._texts)
|
||||
vim.list_extend(chunks, chunk)
|
||||
end
|
||||
end
|
||||
chunks[#chunks + 1] = { "foobar", "Normal" }
|
||||
-- vim.opt.cmdheight = 10
|
||||
-- vim.opt.more = false
|
||||
vim.api.nvim_echo(chunks, true, {})
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,69 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Message = require("noice.message")
|
||||
local Manager = require("noice.message.manager")
|
||||
local Router = require("noice.message.router")
|
||||
local Util = require("noice.util")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@alias NotifyEvent "notify"
|
||||
---@alias NotifyLevel "trace"|"debug"|"info"|"warn"|"error"|"off"
|
||||
|
||||
M._orig = nil
|
||||
|
||||
function M.enable()
|
||||
if vim.notify ~= M.notify then
|
||||
M._orig = vim.notify
|
||||
vim.notify = M.notify
|
||||
end
|
||||
end
|
||||
|
||||
function M.disable()
|
||||
if M._orig then
|
||||
vim.notify = M._orig
|
||||
M._orig = nil
|
||||
end
|
||||
end
|
||||
|
||||
function M.get_level(level)
|
||||
if type(level) == "string" then
|
||||
return level
|
||||
end
|
||||
for k, v in pairs(vim.log.levels) do
|
||||
if v == level then
|
||||
return k:lower()
|
||||
end
|
||||
end
|
||||
return "info"
|
||||
end
|
||||
|
||||
---@param msg string
|
||||
---@param level? number|string
|
||||
---@param opts? table<string, any>
|
||||
function M.notify(msg, level, opts)
|
||||
if vim.in_fast_event() then
|
||||
vim.schedule(function()
|
||||
M.notify(msg, level, opts)
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
level = M.get_level(level)
|
||||
local message = Message("notify", level, msg)
|
||||
message.opts = opts or {}
|
||||
message.level = level
|
||||
|
||||
if msg == nil then
|
||||
-- special case for some destinations like nvim-notify
|
||||
message.opts.is_nil = true
|
||||
end
|
||||
|
||||
Manager.add(message)
|
||||
if Util.is_blocking() then
|
||||
Router.update()
|
||||
end
|
||||
return { id = message.id }
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,216 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Highlight = require("noice.text.highlight")
|
||||
local NuiLine = require("nui.line")
|
||||
local Object = require("nui.object")
|
||||
|
||||
---@alias NoiceChunk { [0]: integer, [1]: string}
|
||||
---@alias NoiceContent string|NoiceChunk|NuiLine|NuiText|NoiceBlock
|
||||
|
||||
---@class NoiceBlock
|
||||
---@field _lines NuiLine[]
|
||||
---@field fix_cr boolean?
|
||||
---@overload fun(content?: NoiceContent|NoiceContent[], highlight?: string|table): NoiceBlock
|
||||
local Block = Object("Block")
|
||||
|
||||
---@param content? NoiceContent|NoiceContent[]
|
||||
---@param highlight? string|table data for highlight
|
||||
function Block:init(content, highlight)
|
||||
self._lines = {}
|
||||
if content then
|
||||
self:append(content, highlight)
|
||||
end
|
||||
end
|
||||
|
||||
function Block:clear()
|
||||
self._lines = {}
|
||||
end
|
||||
|
||||
function Block:content()
|
||||
return table.concat(
|
||||
vim.tbl_map(
|
||||
---@param line NuiLine
|
||||
function(line)
|
||||
return line:content()
|
||||
end,
|
||||
self._lines
|
||||
),
|
||||
"\n"
|
||||
)
|
||||
end
|
||||
|
||||
function Block:width()
|
||||
local ret = 0
|
||||
for _, line in ipairs(self._lines) do
|
||||
ret = math.max(ret, line:width())
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
function Block:length()
|
||||
local ret = 0
|
||||
for _, line in ipairs(self._lines) do
|
||||
ret = ret + line:width()
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
function Block:height()
|
||||
return #self._lines
|
||||
end
|
||||
|
||||
function Block:is_empty()
|
||||
return #self._lines == 0
|
||||
end
|
||||
|
||||
---@param bufnr number buffer number
|
||||
---@param ns_id number namespace id
|
||||
---@param linenr_start? number line number (1-indexed)
|
||||
function Block:highlight(bufnr, ns_id, linenr_start)
|
||||
self:_fix_extmarks()
|
||||
linenr_start = linenr_start or 1
|
||||
Highlight.update()
|
||||
for _, line in ipairs(self._lines) do
|
||||
line:highlight(bufnr, ns_id, linenr_start)
|
||||
linenr_start = linenr_start + 1
|
||||
end
|
||||
end
|
||||
|
||||
function Block:_fix_extmarks()
|
||||
for _, line in ipairs(self._lines) do
|
||||
for _, text in ipairs(line._texts) do
|
||||
if text.extmark then
|
||||
text.extmark.id = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param bufnr number buffer number
|
||||
---@param ns_id number namespace id
|
||||
---@param linenr_start? number start line number (1-indexed)
|
||||
---@param linenr_end? number end line number (1-indexed)
|
||||
function Block:render(bufnr, ns_id, linenr_start, linenr_end)
|
||||
self:_fix_extmarks()
|
||||
linenr_start = linenr_start or 1
|
||||
Highlight.update()
|
||||
for _, line in ipairs(self._lines) do
|
||||
line:render(bufnr, ns_id, linenr_start, linenr_end)
|
||||
linenr_start = linenr_start + 1
|
||||
if linenr_end then
|
||||
linenr_end = linenr_end + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param content string|NuiText|NuiLine
|
||||
---@param highlight? string|table data for highlight
|
||||
---@return NuiText|NuiLine
|
||||
function Block:_append(content, highlight)
|
||||
if #self._lines == 0 then
|
||||
table.insert(self._lines, NuiLine())
|
||||
end
|
||||
if type(content) == "string" and self.fix_cr ~= false then
|
||||
-- handle carriage returns. They overwrite the line from the first character
|
||||
local cr = content:match("^.*()[\r]")
|
||||
if cr then
|
||||
table.remove(self._lines)
|
||||
table.insert(self._lines, NuiLine())
|
||||
content = content:sub(cr + 1)
|
||||
end
|
||||
end
|
||||
return self._lines[#self._lines]:append(content, highlight)
|
||||
end
|
||||
|
||||
---@param contents NoiceContent|NoiceContent[]
|
||||
---@param highlight? string|table data for highlight
|
||||
function Block:set(contents, highlight)
|
||||
self:clear()
|
||||
self:append(contents, highlight)
|
||||
end
|
||||
|
||||
---@param contents NoiceContent|NoiceContent[]
|
||||
---@param highlight? string|table data for highlight
|
||||
function Block:append(contents, highlight)
|
||||
if type(contents) == "string" then
|
||||
contents = { { highlight or 0, contents } }
|
||||
end
|
||||
|
||||
if contents._texts or contents._content or contents._lines or type(contents[1]) == "number" then
|
||||
contents = { contents }
|
||||
end
|
||||
|
||||
---@cast contents NoiceContent[]
|
||||
for _, content in ipairs(contents) do
|
||||
if content._texts then
|
||||
---@cast content NuiLine
|
||||
for _, t in ipairs(content._texts) do
|
||||
self:_append(t)
|
||||
end
|
||||
elseif content._content then
|
||||
---@cast content NuiText
|
||||
self:_append(content)
|
||||
elseif content._lines then
|
||||
---@cast content NoiceBlock
|
||||
for l, line in ipairs(content._lines) do
|
||||
if l == 1 then
|
||||
-- first line should be appended to the existing line
|
||||
self:append(line)
|
||||
else
|
||||
-- other lines are appended as new lines
|
||||
table.insert(self._lines, line)
|
||||
end
|
||||
end
|
||||
else
|
||||
---@cast content NoiceChunk
|
||||
-- Handle newlines
|
||||
---@type number|string|table, string
|
||||
local attr_id, text = unpack(content)
|
||||
-- msg_show messages can contain invalid \r characters
|
||||
if self.fix_cr ~= false then
|
||||
text = text:gsub("%^M", "\r")
|
||||
text = text:gsub("\r\n", "\n")
|
||||
end
|
||||
|
||||
---@type string|table|nil
|
||||
local hl_group
|
||||
if type(attr_id) == "number" then
|
||||
hl_group = attr_id ~= 0 and Highlight.get_hl_group(attr_id) or nil
|
||||
else
|
||||
hl_group = attr_id
|
||||
end
|
||||
|
||||
while text ~= "" do
|
||||
local nl = text:find("\n")
|
||||
local line = nl and text:sub(1, nl - 1) or text
|
||||
self:_append(line, hl_group)
|
||||
if nl then
|
||||
self:newline()
|
||||
text = text:sub(nl + 1)
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Block:last_line()
|
||||
return self._lines[#self._lines]
|
||||
end
|
||||
|
||||
-- trim empty lines at the beginning and the end of the block
|
||||
function Block:trim_empty_lines()
|
||||
while #self._lines > 0 and vim.trim(self._lines[1]:content()) == "" do
|
||||
table.remove(self._lines, 1)
|
||||
end
|
||||
while #self._lines > 0 and vim.trim(self._lines[#self._lines]:content()) == "" do
|
||||
table.remove(self._lines)
|
||||
end
|
||||
end
|
||||
|
||||
function Block:newline()
|
||||
table.insert(self._lines, NuiLine())
|
||||
end
|
||||
|
||||
return Block
|
||||
@ -0,0 +1,198 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Util = require("noice.util")
|
||||
local NoiceText = require("noice.text")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@param message NoiceMessage
|
||||
---@param input NoiceMessage
|
||||
---@param opts NoiceFormatOptions.message
|
||||
function M.message(message, opts, input)
|
||||
if opts.hl_group then
|
||||
message:append(input:content(), opts.hl_group)
|
||||
else
|
||||
message:append(input)
|
||||
end
|
||||
end
|
||||
|
||||
---@param message NoiceMessage
|
||||
---@param opts NoiceFormatOptions.text
|
||||
function M.text(message, opts)
|
||||
if opts.text and opts.text ~= "" then
|
||||
message:append(opts.text, opts.hl_group)
|
||||
end
|
||||
end
|
||||
|
||||
---@param message NoiceMessage
|
||||
---@param opts NoiceFormatOptions.progress
|
||||
function M.progress(message, opts)
|
||||
local contents = require("noice.text.format").format(message, opts.contents, {
|
||||
debug = { enabled = false },
|
||||
})
|
||||
local value = vim.tbl_get(message.opts, unpack(vim.split(opts.key, ".", { plain = true })))
|
||||
if type(value) == "number" then
|
||||
local width = math.max(opts.width, contents:width() + 2)
|
||||
|
||||
local done_length = math.floor(value / 100 * width + 0.5)
|
||||
local todo_length = width - done_length
|
||||
|
||||
if opts.align == "left" then
|
||||
message:append(contents)
|
||||
end
|
||||
|
||||
if width > contents:width() then
|
||||
message:append(string.rep(" ", width - contents:width()))
|
||||
end
|
||||
|
||||
if opts.align == "right" then
|
||||
message:append(contents)
|
||||
end
|
||||
|
||||
message:append(NoiceText("", {
|
||||
hl_group = opts.hl_group_done,
|
||||
hl_mode = "replace",
|
||||
relative = true,
|
||||
col = -width,
|
||||
length = done_length,
|
||||
}))
|
||||
message:append(NoiceText("", {
|
||||
hl_group = opts.hl_group,
|
||||
hl_mode = "replace",
|
||||
relative = true,
|
||||
col = -width + done_length,
|
||||
length = todo_length,
|
||||
}))
|
||||
else
|
||||
message:append(contents)
|
||||
end
|
||||
end
|
||||
|
||||
---@param message NoiceMessage
|
||||
---@param opts NoiceFormatOptions.level
|
||||
function M.level(message, opts)
|
||||
if message.level then
|
||||
local str = message.level:sub(1, 1):upper() .. message.level:sub(2)
|
||||
if opts.icons and opts.icons[message.level] then
|
||||
str = opts.icons[message.level] .. " " .. str
|
||||
end
|
||||
message:append(" " .. str .. " ", opts.hl_group[message.level])
|
||||
end
|
||||
end
|
||||
|
||||
---@param message NoiceMessage
|
||||
---@param opts NoiceFormatOptions.kind
|
||||
function M.kind(message, opts)
|
||||
if message.kind and message.kind ~= "" then
|
||||
message:append(message.kind, opts.hl_group)
|
||||
end
|
||||
end
|
||||
|
||||
---@param message NoiceMessage
|
||||
---@param opts NoiceFormatOptions.title
|
||||
function M.title(message, opts)
|
||||
if message.opts.title then
|
||||
message:append(message.opts.title, opts.hl_group)
|
||||
end
|
||||
end
|
||||
---@param message NoiceMessage
|
||||
---@param opts NoiceFormatOptions.event
|
||||
function M.event(message, opts)
|
||||
if message.event then
|
||||
message:append(message.event, opts.hl_group)
|
||||
end
|
||||
end
|
||||
|
||||
---@param message NoiceMessage
|
||||
---@param opts NoiceFormatOptions.date
|
||||
function M.date(message, opts)
|
||||
message:append(os.date(opts.format, message.ctime), opts.hl_group)
|
||||
end
|
||||
|
||||
---@param message NoiceMessage
|
||||
---@param opts NoiceFormatOptions.debug
|
||||
function M.debug(message, opts)
|
||||
if not opts.enabled then
|
||||
return
|
||||
end
|
||||
local blocking, reason = Util.is_blocking()
|
||||
local debug = {
|
||||
message:is({ cleared = true }) and " " or " ",
|
||||
"#" .. message.id,
|
||||
message.event .. (message.kind and message.kind ~= "" and ("." .. message.kind) or ""),
|
||||
blocking and "⚡ " .. reason,
|
||||
}
|
||||
message:append(NoiceText.virtual_text(" " .. table.concat(
|
||||
vim.tbl_filter(
|
||||
---@param t string
|
||||
function(t)
|
||||
return type(t) == "string"
|
||||
end,
|
||||
debug
|
||||
),
|
||||
" "
|
||||
) .. " ", "DiagnosticVirtualTextInfo"))
|
||||
if message.event == "cmdline" then
|
||||
message:newline()
|
||||
else
|
||||
message:append(" ")
|
||||
end
|
||||
end
|
||||
|
||||
---@param message NoiceMessage
|
||||
---@param opts NoiceFormatOptions.data
|
||||
function M.data(message, opts)
|
||||
local value = vim.tbl_get(message.opts, unpack(vim.split(opts.key, ".", { plain = true })))
|
||||
if value then
|
||||
message:append("" .. value, opts.hl_group)
|
||||
end
|
||||
end
|
||||
|
||||
---@param message NoiceMessage
|
||||
---@param opts NoiceFormatOptions.spinner
|
||||
function M.spinner(message, opts)
|
||||
message:append(require("noice.util.spinners").spin(opts.name), opts.hl_group)
|
||||
end
|
||||
|
||||
---@param message NoiceMessage
|
||||
---@param _opts NoiceFormatOptions.cmdline
|
||||
function M.cmdline(message, _opts)
|
||||
if message.cmdline then
|
||||
message.cmdline:format(message, true)
|
||||
end
|
||||
end
|
||||
|
||||
---@param message NoiceMessage
|
||||
---@param input NoiceMessage
|
||||
---@param opts NoiceFormatOptions.confirm
|
||||
function M.confirm(message, opts, input)
|
||||
if message.kind ~= "confirm" then
|
||||
return message:append(input)
|
||||
end
|
||||
for l, line in ipairs(input._lines) do
|
||||
if l ~= #input._lines then
|
||||
message:append(line)
|
||||
message:newline()
|
||||
end
|
||||
end
|
||||
message:trim_empty_lines()
|
||||
message:newline()
|
||||
message:newline()
|
||||
local _, _, buttons = input:last_line():content():find("(.*):")
|
||||
if buttons then
|
||||
buttons = vim.split(buttons, ", ")
|
||||
|
||||
for b, button in ipairs(buttons) do
|
||||
local hl_group = button:find("%[") and opts.hl_group.default_choice or opts.hl_group.choice
|
||||
message:append(" " .. button .. " ", hl_group)
|
||||
if b ~= #buttons then
|
||||
message:append(" ")
|
||||
end
|
||||
end
|
||||
|
||||
local padding = math.floor((message:width() - message:last_line():width()) / 2)
|
||||
table.insert(message:last_line()._texts, 1, NoiceText((" "):rep(padding)))
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,172 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Util = require("noice.util")
|
||||
local Config = require("noice.config")
|
||||
local FormatConfig = require("noice.config.format")
|
||||
local Formatters = require("noice.text.format.formatters")
|
||||
local NuiText = require("nui.text")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@alias NoiceFormatter fun(message:NoiceMessage, opts: table, input: NoiceMessage): boolean
|
||||
---@alias NoiceFormat (string|table)[]
|
||||
|
||||
---@class NoiceFormatEntry
|
||||
---@field formatter string
|
||||
---@field before? NoiceFormatEntry
|
||||
---@field after? NoiceFormatEntry
|
||||
---@field opts table
|
||||
|
||||
---@param entry string|table<string, any>
|
||||
---@return NoiceFormatEntry?
|
||||
function M.parse_entry(entry)
|
||||
if type(entry) == "string" then
|
||||
entry = { entry }
|
||||
end
|
||||
|
||||
if #entry ~= 1 then
|
||||
Util.panic("Invalid format entry %s", vim.inspect(entry))
|
||||
return
|
||||
end
|
||||
|
||||
local text = entry[1]
|
||||
|
||||
---@type NoiceFormatEntry
|
||||
local ret = {
|
||||
formatter = "text",
|
||||
opts = { text = text },
|
||||
}
|
||||
|
||||
local before, name, after = text:match("^(.*){(.-)}(.*)$")
|
||||
if before then
|
||||
ret.formatter = name
|
||||
ret.before = M.parse_entry(before)
|
||||
ret.after = M.parse_entry(after)
|
||||
end
|
||||
|
||||
local opts_key = ret.formatter:match("^data%.(.*)")
|
||||
if opts_key then
|
||||
entry.key = opts_key
|
||||
ret.formatter = "data"
|
||||
end
|
||||
|
||||
if not Formatters[ret.formatter] then
|
||||
Util.panic("Invalid formatter %s", ret.formatter)
|
||||
return
|
||||
end
|
||||
|
||||
for k, v in pairs(entry) do
|
||||
if k == "before" then
|
||||
ret.before = M.parse_entry(v)
|
||||
elseif k == "after" then
|
||||
ret.after = M.parse_entry(v)
|
||||
elseif type(k) ~= "number" then
|
||||
---@diagnostic disable-next-line: no-unknown
|
||||
ret.opts[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
---@param message NoiceMessage
|
||||
---@param format? NoiceFormat|string
|
||||
---@param opts? NoiceFormatOptions
|
||||
---@return NoiceMessage
|
||||
function M.format(message, format, opts)
|
||||
opts = vim.tbl_deep_extend("force", vim.deepcopy(Config.options.format), opts or {})
|
||||
|
||||
format = format or "default"
|
||||
|
||||
if type(format) == "string" then
|
||||
format = vim.deepcopy(opts[format] or FormatConfig.builtin[format])
|
||||
end
|
||||
|
||||
-- use existing message, with a separate _lines array
|
||||
local ret = setmetatable({ _lines = {}, _debug = false }, { __index = message })
|
||||
if Config.options.debug and not message._debug then
|
||||
table.insert(format, 1, "{debug}")
|
||||
ret._debug = true
|
||||
end
|
||||
|
||||
for _, entry in ipairs(format) do
|
||||
entry = M.parse_entry(entry)
|
||||
if entry then
|
||||
entry.opts = vim.tbl_deep_extend("force", vim.deepcopy(opts[entry.formatter] or {}), entry.opts)
|
||||
|
||||
local formatted = setmetatable({ _lines = {} }, { __index = message })
|
||||
Formatters[entry.formatter](formatted, entry.opts, message)
|
||||
|
||||
if not formatted:is_empty() then
|
||||
if entry.before then
|
||||
Formatters[entry.before.formatter](ret, entry.before.opts, message)
|
||||
end
|
||||
|
||||
ret:append(formatted)
|
||||
|
||||
if entry.after then
|
||||
-- Else, add after
|
||||
Formatters[entry.after.formatter](ret, entry.after.opts, message)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
---@alias NoiceAlign "center" | "left" | "right" | "message-center" | "message-left" | "message-right" | "line-center" | "line-left" | "line-right"
|
||||
|
||||
---@param messages NoiceMessage[]
|
||||
---@param align? NoiceAlign
|
||||
function M.align(messages, align)
|
||||
local width = 0
|
||||
for _, m in ipairs(messages) do
|
||||
for _, line in ipairs(m._lines) do
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
if line._texts[1] and line._texts[1].padding then
|
||||
table.remove(line._texts, 1)
|
||||
end
|
||||
end
|
||||
width = math.max(width, m:width())
|
||||
end
|
||||
|
||||
for _, m in ipairs(messages) do
|
||||
M._align(m, width, align)
|
||||
end
|
||||
end
|
||||
|
||||
---@param message NoiceMessage
|
||||
---@param width integer
|
||||
---@param align? NoiceAlign
|
||||
function M._align(message, width, align)
|
||||
if align == nil or align == "left" then
|
||||
return
|
||||
end
|
||||
|
||||
local align_object = "message"
|
||||
|
||||
---@type string, string
|
||||
local ao, a = align:match("^(.-)%-(.-)$")
|
||||
if a then
|
||||
align = a
|
||||
align_object = ao
|
||||
end
|
||||
|
||||
for _, line in ipairs(message._lines) do
|
||||
local w = align_object == "line" and line:width() or message:width()
|
||||
if w < width then
|
||||
if align == "right" then
|
||||
table.insert(line._texts, 1, NuiText(string.rep(" ", width - w)))
|
||||
---@diagnostic disable-next-line: no-unknown
|
||||
line._texts[1].padding = true
|
||||
elseif align == "center" then
|
||||
table.insert(line._texts, 1, NuiText(string.rep(" ", math.floor((width - w) / 2 + 0.5))))
|
||||
---@diagnostic disable-next-line: no-unknown
|
||||
line._texts[1].padding = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,85 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local ffi = require("noice.util.ffi")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class HLAttrs
|
||||
---@field rgb_ae_attr number
|
||||
---@field rgb_fg_color number
|
||||
---@field rgb_bg_color number
|
||||
---@field rgb_sp_color number
|
||||
---@field hl_blend number
|
||||
|
||||
function M.setup()
|
||||
M.attr2entry = ffi.syn_attr2entry --[[@as fun(attr: number): HLAttrs]]
|
||||
|
||||
vim.api.nvim_create_autocmd("ColorScheme", {
|
||||
pattern = "*",
|
||||
callback = function()
|
||||
M.hl = {}
|
||||
for attr_id, _ in pairs(M.hl_attrs) do
|
||||
M._create_hl(attr_id)
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
---@param attr_id number
|
||||
function M.attr2entry(attr_id)
|
||||
M.setup()
|
||||
return M.attr2entry(attr_id)
|
||||
end
|
||||
|
||||
---@type table<number, number>
|
||||
M.hl = {}
|
||||
|
||||
---@type table<string, table>
|
||||
M.hl_attrs = {}
|
||||
|
||||
---@type table<number, number>
|
||||
M.queue = {}
|
||||
|
||||
function M.get_hl_group(attr_id)
|
||||
if attr_id == 0 then
|
||||
return "Normal"
|
||||
end
|
||||
M.queue[attr_id] = attr_id
|
||||
return "NoiceAttr" .. tostring(attr_id)
|
||||
end
|
||||
|
||||
function M.update()
|
||||
for attr_id, _ in pairs(M.queue) do
|
||||
M._create_hl(attr_id)
|
||||
end
|
||||
M.queue = {}
|
||||
end
|
||||
|
||||
function M._create_hl(attr_id)
|
||||
if attr_id == 0 then
|
||||
return
|
||||
end
|
||||
if not M.hl_attrs[attr_id] then
|
||||
local attrs = M.attr2entry(attr_id)
|
||||
M.hl_attrs[attr_id] = {
|
||||
fg = attrs.rgb_fg_color,
|
||||
bg = attrs.rgb_bg_color,
|
||||
sp = attrs.rgb_sp_color,
|
||||
bold = bit.band(attrs.rgb_ae_attr, 0x02),
|
||||
standout = bit.band(attrs.rgb_ae_attr, 0x0100),
|
||||
italic = bit.band(attrs.rgb_ae_attr, 0x04),
|
||||
underline = bit.band(attrs.rgb_ae_attr, 0x08),
|
||||
undercurl = bit.band(attrs.rgb_ae_attr, 0x10),
|
||||
nocombine = bit.band(attrs.rgb_ae_attr, 0x0200),
|
||||
reverse = bit.band(attrs.rgb_ae_attr, 0x01),
|
||||
blend = attrs.hl_blend ~= -1 and attrs.hl_blend or nil,
|
||||
}
|
||||
end
|
||||
if not M.hl[attr_id] then
|
||||
local hl_group = M.get_hl_group(attr_id)
|
||||
vim.api.nvim_set_hl(0, hl_group, M.hl_attrs[attr_id])
|
||||
M.hl[attr_id] = attr_id
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,124 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local NuiText = require("nui.text")
|
||||
local Treesitter = require("noice.text.treesitter")
|
||||
local Syntax = require("noice.text.syntax")
|
||||
local Markdown = require("noice.text.markdown")
|
||||
|
||||
---@class NoiceExtmark
|
||||
---@field col? number
|
||||
---@field end_col? number
|
||||
---@field id? number
|
||||
---@field hl_group? string
|
||||
---@field virt_self_win_col? number
|
||||
---@field relative? boolean
|
||||
---@field lang? string
|
||||
---@field lines? number
|
||||
|
||||
---@class NoiceText: NuiText
|
||||
---@field super NuiText
|
||||
---@field on_render? fun(text: NoiceText, buf:number, line: number, byte:number, col:number)
|
||||
---@overload fun(content:string, highlight?:string|NoiceExtmark):NoiceText
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local NoiceText = NuiText:extend("NoiceText")
|
||||
|
||||
function NoiceText.virtual_text(text, hl_group)
|
||||
local content = (" "):rep(vim.api.nvim_strwidth(text))
|
||||
return NoiceText(content, {
|
||||
virt_text = { { text, hl_group } },
|
||||
virt_text_win_col = 0,
|
||||
relative = true,
|
||||
})
|
||||
end
|
||||
|
||||
function NoiceText.cursor(col)
|
||||
return NoiceText(" ", {
|
||||
hl_group = "NoiceCursor",
|
||||
col = col,
|
||||
relative = true,
|
||||
})
|
||||
end
|
||||
|
||||
---@param col? number
|
||||
function NoiceText.syntax(lang, lines, col)
|
||||
return NoiceText("", {
|
||||
lang = lang,
|
||||
col = col,
|
||||
lines = lines,
|
||||
})
|
||||
end
|
||||
|
||||
---@param bufnr number buffer number
|
||||
---@param ns_id number namespace id
|
||||
---@param linenr number line number (1-indexed)
|
||||
---@param byte_start number start byte position (0-indexed)
|
||||
---@return nil
|
||||
function NoiceText:highlight(bufnr, ns_id, linenr, byte_start)
|
||||
if not self.extmark then
|
||||
return
|
||||
end
|
||||
|
||||
if self.extmark.lang then
|
||||
local range = { linenr - self.extmark.lines, 0, linenr, byte_start + 1 }
|
||||
if self.extmark.col then
|
||||
range[2] = byte_start + self.extmark.col - 1
|
||||
end
|
||||
if Treesitter.has_lang(self.extmark.lang) then
|
||||
Treesitter.highlight(bufnr, ns_id, range, self.extmark.lang)
|
||||
else
|
||||
Syntax.highlight(bufnr, ns_id, range, self.extmark.lang)
|
||||
end
|
||||
if self.extmark.lang == "markdown" then
|
||||
Markdown.keys(bufnr)
|
||||
Markdown.conceal_escape_characters(bufnr, ns_id, range)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local byte_start_orig = byte_start
|
||||
|
||||
---@type NoiceExtmark
|
||||
local orig = vim.deepcopy(self.extmark)
|
||||
local extmark = self.extmark
|
||||
|
||||
local col_start = 0
|
||||
|
||||
if extmark.relative or self.on_render then
|
||||
---@type string
|
||||
local line = vim.api.nvim_buf_get_text(bufnr, linenr - 1, 0, linenr - 1, byte_start, {})[1]
|
||||
col_start = vim.api.nvim_strwidth(line)
|
||||
end
|
||||
|
||||
if extmark.relative then
|
||||
if extmark.virt_text_win_col then
|
||||
extmark.virt_text_win_col = extmark.virt_text_win_col + col_start
|
||||
end
|
||||
if extmark.col then
|
||||
extmark.col = extmark.col + byte_start
|
||||
end
|
||||
extmark.relative = nil
|
||||
end
|
||||
|
||||
local length = self._length
|
||||
if extmark.length then
|
||||
self._length = extmark.length
|
||||
extmark.length = nil
|
||||
end
|
||||
|
||||
if extmark.col then
|
||||
---@type number
|
||||
byte_start = extmark.col
|
||||
extmark.col = nil
|
||||
end
|
||||
|
||||
NoiceText.super.highlight(self, bufnr, ns_id, linenr, byte_start)
|
||||
|
||||
if self.on_render then
|
||||
self.on_render(self, bufnr, linenr, byte_start_orig, col_start)
|
||||
end
|
||||
|
||||
self._length = length
|
||||
self.extmark = orig
|
||||
end
|
||||
|
||||
return NoiceText
|
||||
@ -0,0 +1,258 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local NoiceText = require("noice.text")
|
||||
local Config = require("noice.config")
|
||||
|
||||
---@alias MarkdownBlock {line:string}
|
||||
---@alias MarkdownCodeBlock {code:string[], lang:string}
|
||||
---@alias Markdown (MarkdownBlock|MarkdownCodeBlock)[]
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.is_rule(line)
|
||||
return line and line:find("^%s*[%*%-_][%*%-_][%*%-_]+%s*$")
|
||||
end
|
||||
|
||||
function M.is_code_block(line)
|
||||
return line and line:find("^%s*```")
|
||||
end
|
||||
|
||||
function M.is_empty(line)
|
||||
return line and line:find("^%s*$")
|
||||
end
|
||||
|
||||
-- TODO:: upstream to treesitter
|
||||
-- ((backslash_escape) @conceal (#set! conceal "_") (#contains? @conceal "\_"))
|
||||
|
||||
---@param text string
|
||||
function M.html_entities(text)
|
||||
local entities = { nbsp = "", lt = "<", gt = ">", amp = "&", quot = '"', apos = "'", ensp = " ", emsp = " " }
|
||||
for entity, char in pairs(entities) do
|
||||
text = text:gsub("&" .. entity .. ";", char)
|
||||
end
|
||||
return text
|
||||
end
|
||||
|
||||
--- test\_foo
|
||||
---@param buf buffer
|
||||
---@param range number[]
|
||||
function M.conceal_escape_characters(buf, ns, range)
|
||||
local chars = "\\`*_{}[]()#+-.!/"
|
||||
local regex = "\\["
|
||||
for i = 1, #chars do
|
||||
regex = regex .. "%" .. chars:sub(i, i)
|
||||
end
|
||||
regex = regex .. "]"
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(buf, range[1], range[3] + 1, false)
|
||||
|
||||
for l, line in ipairs(lines) do
|
||||
local c = line:find(regex)
|
||||
while c do
|
||||
vim.api.nvim_buf_set_extmark(buf, ns, range[1] + l - 1, c - 1, {
|
||||
end_col = c,
|
||||
conceal = "",
|
||||
})
|
||||
c = line:find(regex, c + 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- This is a <code>test</code> **booo**
|
||||
---@param text string
|
||||
---@param opts? MarkdownFormatOptions
|
||||
function M.parse(text, opts)
|
||||
opts = opts or {}
|
||||
---@type string
|
||||
text = text:gsub("</?pre>", "```"):gsub("\r", "")
|
||||
-- text = text:gsub("</?code>", "`")
|
||||
text = M.html_entities(text)
|
||||
|
||||
---@type Markdown
|
||||
local ret = {}
|
||||
|
||||
local lines = vim.split(text, "\n")
|
||||
|
||||
local l = 1
|
||||
|
||||
local function eat_nl()
|
||||
while M.is_empty(lines[l + 1]) do
|
||||
l = l + 1
|
||||
end
|
||||
end
|
||||
|
||||
while l <= #lines do
|
||||
local line = lines[l]
|
||||
if M.is_empty(line) then
|
||||
local is_start = l == 1
|
||||
eat_nl()
|
||||
local is_end = l == #lines
|
||||
if not (M.is_code_block(lines[l + 1]) or M.is_rule(lines[l + 1]) or is_start or is_end) then
|
||||
table.insert(ret, { line = "" })
|
||||
end
|
||||
elseif M.is_code_block(line) then
|
||||
---@type string
|
||||
local lang = line:match("```%s*(%S+)") or opts.ft or "text"
|
||||
local block = { lang = lang, code = {} }
|
||||
while lines[l + 1] and not M.is_code_block(lines[l + 1]) do
|
||||
table.insert(block.code, lines[l + 1])
|
||||
l = l + 1
|
||||
end
|
||||
|
||||
local prev = ret[#ret]
|
||||
if prev and not M.is_rule(prev.line) then
|
||||
table.insert(ret, { line = "" })
|
||||
end
|
||||
|
||||
table.insert(ret, block)
|
||||
l = l + 1
|
||||
eat_nl()
|
||||
elseif M.is_rule(line) then
|
||||
table.insert(ret, { line = "---" })
|
||||
eat_nl()
|
||||
else
|
||||
local prev = ret[#ret]
|
||||
if prev and prev.code then
|
||||
table.insert(ret, { line = "" })
|
||||
end
|
||||
table.insert(ret, { line = line })
|
||||
end
|
||||
l = l + 1
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
function M.get_highlights(line)
|
||||
---@type NoiceText[]
|
||||
local ret = {}
|
||||
for pattern, hl_group in pairs(Config.options.markdown.highlights) do
|
||||
local from = 1
|
||||
while from do
|
||||
---@type number, string?
|
||||
local to, match
|
||||
---@type number, number, string?
|
||||
from, to, match = line:find(pattern, from)
|
||||
if match then
|
||||
---@type number, number
|
||||
from, to = line:find(match, from)
|
||||
end
|
||||
if from then
|
||||
table.insert(
|
||||
ret,
|
||||
NoiceText("", {
|
||||
hl_group = hl_group,
|
||||
col = from - 1,
|
||||
length = to - from + 1,
|
||||
-- priority = 120,
|
||||
})
|
||||
)
|
||||
end
|
||||
from = to and to + 1 or nil
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
---@alias MarkdownFormatOptions {ft?: string}
|
||||
|
||||
---@param message NoiceMessage
|
||||
---@param text string
|
||||
---@param opts? MarkdownFormatOptions
|
||||
--```lua
|
||||
--local a = 1
|
||||
--local b = true
|
||||
--```
|
||||
--foo tex
|
||||
function M.format(message, text, opts)
|
||||
opts = opts or {}
|
||||
|
||||
local blocks = M.parse(text, opts)
|
||||
|
||||
local md_lines = 0
|
||||
|
||||
local function emit_md()
|
||||
if md_lines > 0 then
|
||||
message:append(NoiceText.syntax("markdown", md_lines))
|
||||
md_lines = 0
|
||||
end
|
||||
end
|
||||
|
||||
for l = 1, #blocks do
|
||||
local block = blocks[l]
|
||||
if block.code then
|
||||
emit_md()
|
||||
message:newline()
|
||||
---@cast block MarkdownCodeBlock
|
||||
for c, line in ipairs(block.code) do
|
||||
message:append(line)
|
||||
if c == #block.code then
|
||||
message:append(NoiceText.syntax(block.lang, #block.code))
|
||||
else
|
||||
message:newline()
|
||||
end
|
||||
end
|
||||
else
|
||||
---@cast block MarkdownBlock
|
||||
message:newline()
|
||||
if M.is_rule(block.line) then
|
||||
M.horizontal_line(message)
|
||||
else
|
||||
message:append(block.line)
|
||||
for _, t in ipairs(M.get_highlights(block.line)) do
|
||||
message:append(t)
|
||||
end
|
||||
md_lines = md_lines + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
emit_md()
|
||||
end
|
||||
|
||||
function M.keys(buf)
|
||||
if not vim.api.nvim_buf_is_valid(buf) then
|
||||
return
|
||||
end
|
||||
if vim.b[buf].markdown_keys then
|
||||
return
|
||||
end
|
||||
|
||||
local function map(lhs)
|
||||
vim.keymap.set("n", lhs, function()
|
||||
local line = vim.api.nvim_get_current_line()
|
||||
local pos = vim.api.nvim_win_get_cursor(0)
|
||||
local col = pos[2] + 1
|
||||
|
||||
for pattern, handler in pairs(Config.options.markdown.hover) do
|
||||
local from = 1
|
||||
local to, url
|
||||
while from do
|
||||
from, to, url = line:find(pattern, from)
|
||||
if from and col >= from and col <= to then
|
||||
return handler(url)
|
||||
end
|
||||
if from then
|
||||
from = to + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
vim.api.nvim_feedkeys(lhs, "n", false)
|
||||
end, { buffer = buf, silent = true })
|
||||
end
|
||||
|
||||
map("gx")
|
||||
map("K")
|
||||
|
||||
vim.b[buf].markdown_keys = true
|
||||
end
|
||||
|
||||
---@param message NoiceMessage
|
||||
function M.horizontal_line(message)
|
||||
message:append(NoiceText("", {
|
||||
virt_text_win_col = 0,
|
||||
virt_text = { { string.rep("─", vim.go.columns), "@punctuation.special.markdown" } },
|
||||
priority = 100,
|
||||
}))
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,29 @@
|
||||
local M = {}
|
||||
|
||||
--- Highlights a region of the buffer with a given language
|
||||
---@param buf buffer buffer to highlight. Defaults to the current buffer if 0
|
||||
---@param ns number namespace for the highlights
|
||||
---@param range {[1]:number, [2]:number, [3]: number, [4]: number} (table) Region to highlight {start_row, start_col, end_row, end_col}
|
||||
---@param lang string treesitter language
|
||||
function M.highlight(buf, ns, range, lang)
|
||||
vim.api.nvim_buf_call(buf, function()
|
||||
local group = "@" .. lang:upper()
|
||||
|
||||
-- HACK: reset current_syntax, since some syntax files like markdown won't load if it is already set
|
||||
pcall(vim.api.nvim_buf_del_var, buf, "current_syntax")
|
||||
if not pcall(vim.cmd, string.format("syntax include %s syntax/%s.vim", group, lang)) then
|
||||
return
|
||||
end
|
||||
vim.cmd(
|
||||
string.format(
|
||||
"syntax region %s start=+\\%%%dl+ end=+\\%%%dl+ contains=%s keepend",
|
||||
lang .. range[1],
|
||||
range[1] + 1,
|
||||
range[3] + 1,
|
||||
group
|
||||
)
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,83 @@
|
||||
local M = {}
|
||||
M.queries = {}
|
||||
|
||||
function M.get_query(lang)
|
||||
if not M.queries[lang] then
|
||||
---@diagnostic disable-next-line: deprecated
|
||||
M.queries[lang] = (vim.treesitter.query.get or vim.treesitter.query.get_query)(lang, "highlights")
|
||||
end
|
||||
return M.queries[lang]
|
||||
end
|
||||
|
||||
function M.get_lang(ft)
|
||||
return vim.treesitter.language.get_lang and vim.treesitter.language.get_lang(ft) or ft
|
||||
end
|
||||
|
||||
function M.has_lang(lang)
|
||||
if vim.treesitter.language.get_lang then
|
||||
lang = vim.treesitter.language.get_lang(lang) or lang
|
||||
return pcall(vim.treesitter.language.add, lang)
|
||||
end
|
||||
---@diagnostic disable-next-line: deprecated
|
||||
return vim.treesitter.language.require_language(lang, nil, true)
|
||||
end
|
||||
|
||||
--- Highlights a region of the buffer with a given language
|
||||
---@param buf integer? buffer to highlight. Defaults to the current buffer if 0
|
||||
---@param ns number namespace for the highlights
|
||||
---@param range {[1]:number, [2]:number, [3]: number, [4]: number} (table) Region to highlight {start_row, start_col, end_row, end_col}
|
||||
---@param lang string treesitter language
|
||||
-- luacheck: no redefined
|
||||
function M.highlight(buf, ns, range, lang)
|
||||
lang = M.get_lang(lang)
|
||||
|
||||
buf = (buf == 0 or buf == nil) and vim.api.nvim_get_current_buf() or buf
|
||||
|
||||
-- we can't use a cached parser here since that could interfere with the existing parser of the buffer
|
||||
local LanguageTree = require("vim.treesitter.languagetree")
|
||||
local opts = { injections = { php = "", html = "" } }
|
||||
local parser = LanguageTree.new(buf --[[@as integer]], lang, opts)
|
||||
|
||||
---@diagnostic disable-next-line: invisible
|
||||
parser:set_included_regions({ { range } })
|
||||
parser:parse(true)
|
||||
|
||||
parser:for_each_tree(function(tstree, tree)
|
||||
if not tstree then
|
||||
return
|
||||
end
|
||||
local highlighter_query = M.get_query(tree:lang())
|
||||
|
||||
-- Some injected languages may not have highlight queries.
|
||||
if not highlighter_query then
|
||||
return
|
||||
end
|
||||
|
||||
local iter = highlighter_query:iter_captures(tstree:root(), buf, range[1], range[3])
|
||||
|
||||
for capture, node, metadata in iter do
|
||||
---@type number, number, number, number
|
||||
local start_row, start_col, end_row, end_col = node:range()
|
||||
|
||||
---@type string
|
||||
local name = highlighter_query.captures[capture]
|
||||
local hl = 0
|
||||
if not vim.startswith(name, "_") then
|
||||
hl = vim.api.nvim_get_hl_id_by_name("@" .. name .. "." .. lang)
|
||||
end
|
||||
local is_spell = name == "spell"
|
||||
|
||||
if hl and not is_spell then
|
||||
pcall(vim.api.nvim_buf_set_extmark, buf, ns, start_row, start_col, {
|
||||
end_line = end_row,
|
||||
end_col = end_col,
|
||||
hl_group = hl,
|
||||
priority = (tonumber(metadata.priority) or 100) + 10, -- add 10, so it will be higher than the standard highlighter of the buffer
|
||||
conceal = metadata.conceal,
|
||||
})
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,54 @@
|
||||
---@class NuiRelative
|
||||
---@field type "'cursor'"|"'editor'"|"'win'"
|
||||
---@field winid? number
|
||||
---@field position? { row: number, col: number }
|
||||
|
||||
---@alias _.NuiBorderStyle "'double'"|"'none'"|"'rounded'"|"'shadow'"|"'single'"|"'solid'"
|
||||
|
||||
---@alias _.NuiBorderPadding {top:number, right:number, bottom:number, left:number}
|
||||
|
||||
---@class _.NuiBorder
|
||||
---@field padding? _.NuiBorderPadding
|
||||
---@field style? _.NuiBorderStyle
|
||||
---@field text? { top: string|boolean, bottom: string|boolean }
|
||||
|
||||
---@class NuiBorder: _.NuiBorder
|
||||
---@field padding? _.NuiBorderPadding|number[]
|
||||
|
||||
---@class _.NuiBaseOptions
|
||||
---@field relative? NuiRelative
|
||||
---@field enter? boolean
|
||||
---@field timeout? number
|
||||
---@field buf_options? vim.bo
|
||||
---@field win_options? vim.wo
|
||||
---@field close? {events?:string[], keys?:string[]}
|
||||
|
||||
---@class NuiBaseOptions: _.NuiBaseOptions
|
||||
---@field relative "'cursor'"|"'editor'"|"'win'"|NuiRelative
|
||||
|
||||
---@alias NuiAnchor "NW"|"NE"|"SW"|"SE"
|
||||
|
||||
---@class _.NuiPopupOptions: _.NuiBaseOptions
|
||||
---@field position { row: number|string, col: number|string}
|
||||
---@field size { width: number|string, height: number|string, max_width:number, max_height:number}
|
||||
---@field border? _.NuiBorder
|
||||
---@field anchor? NuiAnchor|"auto"
|
||||
---@field focusable boolean
|
||||
---@field zindex? number
|
||||
|
||||
---@class NuiPopupOptions: NuiBaseOptions,_.NuiPopupOptions
|
||||
---@field position number|string|{ row: number|string, col: number|string}
|
||||
---@field size number|string|{ row: number|string, col: number|string}
|
||||
---@field border? NuiBorder|_.NuiBorderStyle
|
||||
|
||||
---@class _.NuiSplitOptions: _.NuiBaseOptions
|
||||
---@field position "top"|"right"|"bottom"|"left"
|
||||
---@field scrollbar? boolean
|
||||
---@field min_size? number
|
||||
---@field max_size? number
|
||||
---@field size number|string
|
||||
|
||||
---@class NuiSplitOptions: NuiBaseOptions,_.NuiSplitOptions
|
||||
|
||||
---@alias NoiceNuiOptions NuiSplitOptions|NuiPopupOptions|{type: "split"|"popup"}
|
||||
---@alias _.NoiceNuiOptions _.NuiSplitOptions|_.NuiPopupOptions|{type: "split"|"popup"}
|
||||
@ -0,0 +1,276 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Message = require("noice.message")
|
||||
local Manager = require("noice.message.manager")
|
||||
local Config = require("noice.config")
|
||||
local NoiceText = require("noice.text")
|
||||
local Hacks = require("noice.util.hacks")
|
||||
local Object = require("nui.object")
|
||||
|
||||
local M = {}
|
||||
M.message = Message("cmdline", nil)
|
||||
|
||||
---@enum CmdlineEvent
|
||||
M.events = {
|
||||
cmdline = "cmdline",
|
||||
show = "cmdline_show",
|
||||
hide = "cmdline_hide",
|
||||
pos = "cmdline_pos",
|
||||
special_char = "cmdline_special_char",
|
||||
block_show = "cmdline_block_show",
|
||||
block_append = "cmdline_block_append",
|
||||
block_hide = "cmdline_block_hide",
|
||||
}
|
||||
|
||||
---@type NoiceCmdline?
|
||||
M.active = nil
|
||||
|
||||
---@alias NoiceCmdlineFormatter fun(cmdline: NoiceCmdline): {icon?:string, offset?:number, view?:NoiceViewOptions}
|
||||
|
||||
---@class CmdlineState
|
||||
---@field content {[1]: integer, [2]: string}[]
|
||||
---@field pos number
|
||||
---@field firstc string
|
||||
---@field prompt string
|
||||
---@field indent number
|
||||
---@field level number
|
||||
---@field block table
|
||||
|
||||
---@class CmdlineFormat
|
||||
---@field kind string
|
||||
---@field pattern? string|string[]
|
||||
---@field view string
|
||||
---@field conceal? boolean
|
||||
---@field icon? string
|
||||
---@field icon_hl_group? string
|
||||
---@field opts? NoiceViewOptions
|
||||
---@field title? string
|
||||
---@field lang? string
|
||||
|
||||
---@class NoiceCmdline
|
||||
---@field state CmdlineState
|
||||
---@field offset integer
|
||||
---@overload fun(state:CmdlineState): NoiceCmdline
|
||||
local Cmdline = Object("NoiceCmdline")
|
||||
|
||||
---@param state CmdlineState
|
||||
function Cmdline:init(state)
|
||||
self.state = state or {}
|
||||
self.offset = 0
|
||||
end
|
||||
|
||||
function Cmdline:get()
|
||||
return table.concat(
|
||||
vim.tbl_map(function(c)
|
||||
return c[2]
|
||||
end, self.state.content),
|
||||
""
|
||||
)
|
||||
end
|
||||
|
||||
---@return CmdlineFormat
|
||||
function Cmdline:get_format()
|
||||
if self.state.prompt and self.state.prompt ~= "" then
|
||||
return Config.options.cmdline.format.input
|
||||
end
|
||||
local line = self.state.firstc .. self:get()
|
||||
|
||||
---@type {offset:number, format: CmdlineFormat}[]
|
||||
local ret = {}
|
||||
|
||||
for _, format in pairs(Config.options.cmdline.format) do
|
||||
local patterns = type(format.pattern) == "table" and format.pattern or { format.pattern }
|
||||
---@cast patterns string[]
|
||||
for _, pattern in ipairs(patterns) do
|
||||
local from, to = line:find(pattern)
|
||||
-- if match and cmdline pos is visible
|
||||
if from and self.state.pos >= to - 1 then
|
||||
ret[#ret + 1] = {
|
||||
offset = to or 0,
|
||||
format = format,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(ret, function(a, b)
|
||||
return a.offset > b.offset
|
||||
end)
|
||||
local format = ret[1]
|
||||
if format then
|
||||
self.offset = format.format.conceal and format.offset or 0
|
||||
return format.format
|
||||
end
|
||||
self.offset = 0
|
||||
return {
|
||||
kind = self.state.firstc,
|
||||
view = "cmdline_popup",
|
||||
}
|
||||
end
|
||||
|
||||
---@param message NoiceMessage
|
||||
---@param text_only? boolean
|
||||
function Cmdline:format(message, text_only)
|
||||
local format = self:get_format()
|
||||
message.fix_cr = false
|
||||
|
||||
if format.icon then
|
||||
message:append(NoiceText.virtual_text(format.icon, format.icon_hl_group))
|
||||
message:append(" ")
|
||||
end
|
||||
|
||||
if not text_only then
|
||||
message.kind = format.kind
|
||||
end
|
||||
|
||||
-- FIXME: prompt
|
||||
if self.state.prompt ~= "" then
|
||||
message:append(self.state.prompt, "NoiceCmdlinePrompt")
|
||||
end
|
||||
|
||||
if not format.conceal then
|
||||
message:append(self.state.firstc)
|
||||
end
|
||||
|
||||
local cmd = self:get():sub(self.offset)
|
||||
|
||||
message:append(cmd)
|
||||
|
||||
if format.lang then
|
||||
message:append(NoiceText.syntax(format.lang, 1, -vim.fn.strlen(cmd)))
|
||||
end
|
||||
|
||||
if not text_only then
|
||||
local cursor = NoiceText.cursor(-self:length() + self.state.pos)
|
||||
cursor.on_render = M.on_render
|
||||
message:append(cursor)
|
||||
end
|
||||
end
|
||||
|
||||
function Cmdline:width()
|
||||
return vim.api.nvim_strwidth(self:get())
|
||||
end
|
||||
|
||||
function Cmdline:length()
|
||||
return vim.fn.strlen(self:get())
|
||||
end
|
||||
|
||||
---@type NoiceCmdline[]
|
||||
M.cmdlines = {}
|
||||
M.skipped = false
|
||||
|
||||
function M.on_show(event, content, pos, firstc, prompt, indent, level)
|
||||
local c = Cmdline({
|
||||
event = event,
|
||||
content = content,
|
||||
pos = pos,
|
||||
firstc = firstc,
|
||||
prompt = prompt,
|
||||
indent = indent,
|
||||
level = level,
|
||||
})
|
||||
|
||||
-- This was triggered by a force redraw, so skip it
|
||||
if c:get():find(Hacks.SPECIAL, 1, true) then
|
||||
M.skipped = true
|
||||
return
|
||||
end
|
||||
M.skipped = false
|
||||
|
||||
local last = M.cmdlines[level] and M.cmdlines[level].state
|
||||
if not vim.deep_equal(c.state, last) then
|
||||
M.active = c
|
||||
M.cmdlines[level] = c
|
||||
M.update()
|
||||
end
|
||||
end
|
||||
|
||||
function M.on_hide(_, level)
|
||||
if M.cmdlines[level] then
|
||||
M.cmdlines[level] = nil
|
||||
local active = M.active
|
||||
vim.defer_fn(function()
|
||||
if M.active == active then
|
||||
M.active = nil
|
||||
end
|
||||
end, 100)
|
||||
M.update()
|
||||
end
|
||||
end
|
||||
|
||||
function M.on_pos(_, pos, level)
|
||||
if M.skipped then
|
||||
return
|
||||
end
|
||||
local c = M.cmdlines[level]
|
||||
if c and c.state.pos ~= pos then
|
||||
M.cmdlines[level].state.pos = pos
|
||||
M.update()
|
||||
end
|
||||
end
|
||||
|
||||
---@class CmdlinePosition
|
||||
---@field win number Window containing the cmdline
|
||||
---@field buf number Buffer containing the cmdline
|
||||
---@field bufpos {row:number, col:number} (1-0)-indexed position of the cmdline in the buffer
|
||||
---@field screenpos {row:number, col:number} (1-0)-indexed screen position of the cmdline
|
||||
M.position = nil
|
||||
|
||||
---@param buf number
|
||||
---@param line number
|
||||
---@param byte number
|
||||
function M.on_render(_, buf, line, byte)
|
||||
Hacks.cmdline_force_redraw()
|
||||
local win = vim.fn.bufwinid(buf)
|
||||
if win ~= -1 then
|
||||
-- FIXME: check with cmp
|
||||
-- FIXME: state.pos?
|
||||
local cmdline_start = byte - (M.last():length() - M.last().offset)
|
||||
|
||||
local cursor = byte - M.last():length() + M.last().state.pos
|
||||
vim.schedule(function()
|
||||
if vim.api.nvim_win_is_valid(win) then
|
||||
vim.api.nvim_win_set_cursor(win, { 1, cursor })
|
||||
vim.api.nvim_win_call(win, function()
|
||||
local width = vim.api.nvim_win_get_width(win)
|
||||
local leftcol = math.max(cursor - width + 1, 0)
|
||||
vim.fn.winrestview({ leftcol = leftcol })
|
||||
end)
|
||||
end
|
||||
end)
|
||||
|
||||
local pos = vim.fn.screenpos(win, line, cmdline_start)
|
||||
M.position = {
|
||||
buf = buf,
|
||||
win = win,
|
||||
bufpos = {
|
||||
row = line,
|
||||
col = cmdline_start,
|
||||
},
|
||||
screenpos = {
|
||||
row = pos.row,
|
||||
col = pos.col - 1,
|
||||
},
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
function M.last()
|
||||
local last = math.max(1, unpack(vim.tbl_keys(M.cmdlines)))
|
||||
return M.cmdlines[last]
|
||||
end
|
||||
|
||||
function M.update()
|
||||
M.message:clear()
|
||||
local cmdline = M.last()
|
||||
|
||||
if cmdline then
|
||||
cmdline:format(M.message)
|
||||
Hacks.hide_cursor()
|
||||
Manager.add(M.message)
|
||||
else
|
||||
Manager.remove(M.message)
|
||||
Hacks.show_cursor()
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,5 @@
|
||||
local M = {}
|
||||
|
||||
function M.on_destroy() end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,193 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Config = require("noice.config")
|
||||
local Util = require("noice.util")
|
||||
local Router = require("noice.message.router")
|
||||
local Manager = require("noice.message.manager")
|
||||
|
||||
---@alias NoiceEvent MsgEvent|CmdlineEvent|NotifyEvent|LspEvent
|
||||
---@alias NoiceKind MsgKind|NotifyLevel|LspKind
|
||||
|
||||
local M = {}
|
||||
M._attached = false
|
||||
|
||||
---@type table<string, table|false>
|
||||
M._handlers = {}
|
||||
|
||||
function M.setup()
|
||||
local widgets = {
|
||||
messages = "msg",
|
||||
cmdline = "cmdline",
|
||||
popupmenu = "popupmenu",
|
||||
}
|
||||
|
||||
-- Check if we're running inside a GUI that already externalizes some widgets
|
||||
---@type table<string, boolean>
|
||||
local ui_widgets = {}
|
||||
local uis = vim.api.nvim_list_uis()
|
||||
for _, ui in ipairs(uis) do
|
||||
for ext, _ in pairs(widgets) do
|
||||
if ui["ext_" .. ext] then
|
||||
ui_widgets[ext] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
M._handlers = {}
|
||||
|
||||
---@type table<string, boolean>
|
||||
local options = {}
|
||||
for ext, widget in pairs(widgets) do
|
||||
-- only enable if configured and not enabled in the GUI
|
||||
if Config.options[ext].enabled and not ui_widgets[ext] then
|
||||
options["ext_" .. ext] = true
|
||||
M._handlers[widget] = _G.require("noice.ui." .. widget)
|
||||
else
|
||||
if ui_widgets[ext] and Config.options.debug then
|
||||
Util.warn("Disabling ext_" .. ext)
|
||||
end
|
||||
M._handlers[widget] = false
|
||||
end
|
||||
end
|
||||
return options
|
||||
end
|
||||
|
||||
function M.enable()
|
||||
local options = M.setup()
|
||||
|
||||
if vim.tbl_isempty(options) then
|
||||
if Config.options.debug then
|
||||
vim.schedule(function()
|
||||
Util.warn("No extensions enabled")
|
||||
end)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if options.ext_messages then
|
||||
require("noice.ui.msg").setup()
|
||||
end
|
||||
|
||||
local safe_handle = Util.protect(M.handle, { msg = "An error happened while handling a ui event" })
|
||||
M._attached = true
|
||||
|
||||
local stack_level = 0
|
||||
|
||||
---@diagnostic disable-next-line: redundant-parameter
|
||||
vim.ui_attach(Config.ns, options, function(event, kind, ...)
|
||||
if Util.is_exiting() then
|
||||
return true
|
||||
end
|
||||
|
||||
local handler = M.get_handler(event, kind, ...)
|
||||
|
||||
if not handler then
|
||||
return
|
||||
end
|
||||
|
||||
if stack_level > 50 then
|
||||
Util.panic("Event loop detected. Shutting down...")
|
||||
return true
|
||||
end
|
||||
stack_level = stack_level + 1
|
||||
|
||||
local tick = Manager.tick()
|
||||
safe_handle(handler, event, kind, ...)
|
||||
|
||||
-- check if we need to update the ui
|
||||
if Manager.tick() > tick then
|
||||
Util.debug(function()
|
||||
local _, blocking = Util.is_blocking()
|
||||
return { event, "sl:" .. stack_level, "tick:" .. tick, blocking or false, kind }
|
||||
end)
|
||||
if
|
||||
require("noice.util.ffi").textlock == 0
|
||||
and Util.is_blocking()
|
||||
and event ~= "msg_ruler"
|
||||
and kind ~= "search_count"
|
||||
then
|
||||
Util.try(Router.update)
|
||||
end
|
||||
else
|
||||
local widget = M.parse_event(event)
|
||||
Util.stats.track(widget .. ".skipped")
|
||||
end
|
||||
stack_level = stack_level - 1
|
||||
|
||||
-- make sure only Noice handles these events
|
||||
return true
|
||||
end)
|
||||
|
||||
vim.api.nvim_create_autocmd("SwapExists", {
|
||||
group = vim.api.nvim_create_augroup("noice-swap-exists", { clear = true }),
|
||||
callback = function()
|
||||
Util.try(Router.update)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
function M.redirect()
|
||||
M.disable()
|
||||
Router.echo_pending()
|
||||
vim.schedule(M.enable)
|
||||
end
|
||||
|
||||
function M.disable()
|
||||
if M._attached then
|
||||
M._attached = false
|
||||
vim.ui_detach(Config.ns)
|
||||
end
|
||||
end
|
||||
|
||||
---@return string, string
|
||||
function M.parse_event(event)
|
||||
return event:match("([a-z]+)_(.*)")
|
||||
end
|
||||
|
||||
---@param event string
|
||||
function M.get_handler(event, ...)
|
||||
local event_group, event_type = event:match("([a-z]+)_(.*)")
|
||||
local on = "on_" .. event_type
|
||||
|
||||
local handler = M._handlers[event_group]
|
||||
|
||||
-- false means this is a disabled handler
|
||||
if handler == false then
|
||||
return
|
||||
end
|
||||
|
||||
if not handler then
|
||||
if Config.options.debug then
|
||||
vim.schedule(function()
|
||||
Util.error_once("No ui router for " .. event_group)
|
||||
end)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if type(handler[on]) ~= "function" then
|
||||
local args = { ... }
|
||||
if Config.options.debug then
|
||||
vim.schedule(function()
|
||||
Util.error_once(
|
||||
"No ui router for **"
|
||||
.. event
|
||||
.. "** events\n```lua\n"
|
||||
.. vim.inspect({ group = event_group, on = on, args = args })
|
||||
.. "\n```"
|
||||
)
|
||||
end)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
return handler[on]
|
||||
end
|
||||
|
||||
---@param handler fun(...)
|
||||
---@param event string
|
||||
function M.handle(handler, event, ...)
|
||||
handler(event, ...)
|
||||
end
|
||||
|
||||
return M
|
||||
182
config/neovim/store/lazy-plugins/noice.nvim/lua/noice/ui/msg.lua
Normal file
182
config/neovim/store/lazy-plugins/noice.nvim/lua/noice/ui/msg.lua
Normal file
@ -0,0 +1,182 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Manager = require("noice.message.manager")
|
||||
local Message = require("noice.message")
|
||||
local Hacks = require("noice.util.hacks")
|
||||
local State = require("noice.ui.state")
|
||||
local Cmdline = require("noice.ui.cmdline")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@enum MsgEvent
|
||||
M.events = {
|
||||
show = "msg_show",
|
||||
clear = "msg_clear",
|
||||
showmode = "msg_showmode",
|
||||
showcmd = "msg_showcmd",
|
||||
ruler = "msg_ruler",
|
||||
history_show = "msg_history_show",
|
||||
history_clear = "msg_history_clear",
|
||||
}
|
||||
|
||||
---@enum MsgKind
|
||||
M.kinds = {
|
||||
-- echo
|
||||
empty = "", -- (empty) Unknown (consider a feature-request: |bugs|)
|
||||
echo = "echo", -- |:echo| message
|
||||
echomsg = "echomsg", -- |:echomsg| message
|
||||
-- input related
|
||||
confirm = "confirm", -- |confirm()| or |:confirm| dialog
|
||||
confirm_sub = "confirm_sub", -- |:substitute| confirm dialog |:s_c|
|
||||
return_prompt = "return_prompt", -- |press-enter| prompt after a multiple messages
|
||||
-- error/warnings
|
||||
emsg = "emsg", -- Error (|errors|, internal error, |:throw|, …)
|
||||
echoerr = "echoerr", -- |:echoerr| message
|
||||
lua_error = "lua_error", -- Error in |:lua| code
|
||||
rpc_error = "rpc_error", -- Error response from |rpcrequest()|
|
||||
wmsg = "wmsg", -- Warning ("search hit BOTTOM", |W10|, …)
|
||||
-- hints
|
||||
quickfix = "quickfix", -- Quickfix navigation message
|
||||
search_count = "search_count", -- Search count message ("S" flag of 'shortmess')
|
||||
}
|
||||
|
||||
---@type NoiceMessage
|
||||
M.last = nil
|
||||
---@type NoiceMessage[]
|
||||
M._messages = {}
|
||||
|
||||
M._did_setup = false
|
||||
|
||||
function M.setup()
|
||||
if M._did_setup then
|
||||
return
|
||||
end
|
||||
M._did_setup = true
|
||||
|
||||
local hist = vim.trim(vim.api.nvim_cmd({ cmd = "messages" }, { output = true }))
|
||||
if hist == "" then
|
||||
return
|
||||
end
|
||||
local message = M.get(M.events.show)
|
||||
message:set(hist)
|
||||
Manager.add(message)
|
||||
end
|
||||
|
||||
function M.is_error(kind)
|
||||
return vim.tbl_contains({ M.kinds.echoerr, M.kinds.lua_error, M.kinds.rpc_error, M.kinds.emsg }, kind)
|
||||
end
|
||||
|
||||
function M.is_warning(kind)
|
||||
return kind == M.kinds.wmsg
|
||||
end
|
||||
|
||||
function M.get(event, kind)
|
||||
local id = event .. "." .. (kind or "")
|
||||
if not M._messages[id] then
|
||||
M._messages[id] = Message(event, kind)
|
||||
end
|
||||
return M._messages[id]
|
||||
end
|
||||
|
||||
---@param kind MsgKind
|
||||
---@param content NoiceContent[]
|
||||
function M.on_show(event, kind, content, replace_last)
|
||||
if kind == M.kinds.return_prompt then
|
||||
return M.on_return_prompt()
|
||||
elseif kind == M.kinds.confirm or kind == M.kinds.confirm_sub then
|
||||
return M.on_confirm(event, kind, content)
|
||||
end
|
||||
|
||||
if State.skip(event, kind, content, replace_last) then
|
||||
return
|
||||
end
|
||||
|
||||
if M.last and replace_last then
|
||||
Manager.clear({ message = M.last })
|
||||
M.last = nil
|
||||
end
|
||||
|
||||
local message
|
||||
if kind == M.kinds.search_count then
|
||||
message = M.get(event, kind)
|
||||
Hacks.fix_nohlsearch()
|
||||
else
|
||||
message = Message(event, kind)
|
||||
message.cmdline = Cmdline.active
|
||||
end
|
||||
|
||||
message:set(content)
|
||||
|
||||
message:trim_empty_lines()
|
||||
if M.is_error(kind) then
|
||||
message.level = "error"
|
||||
elseif M.is_warning(kind) then
|
||||
message.level = "warn"
|
||||
end
|
||||
|
||||
M.last = message
|
||||
|
||||
Manager.add(message)
|
||||
end
|
||||
|
||||
function M.on_clear()
|
||||
State.clear("msg_show")
|
||||
M.last = nil
|
||||
local message = M.get(M.events.show, M.kinds.search_count)
|
||||
Manager.remove(message)
|
||||
end
|
||||
|
||||
-- mode like recording...
|
||||
function M.on_showmode(event, content)
|
||||
if State.skip(event, content) then
|
||||
return
|
||||
end
|
||||
local message = M.get(event)
|
||||
if vim.tbl_isempty(content) then
|
||||
if event == "msg_showmode" then
|
||||
Manager.remove(message)
|
||||
end
|
||||
else
|
||||
message:set(content)
|
||||
Manager.add(message)
|
||||
end
|
||||
end
|
||||
M.on_showcmd = M.on_showmode
|
||||
M.on_ruler = M.on_showmode
|
||||
|
||||
function M.on_return_prompt()
|
||||
return vim.api.nvim_input("<cr>")
|
||||
end
|
||||
|
||||
---@param content NoiceChunk[]
|
||||
function M.on_confirm(event, kind, content)
|
||||
if State.skip(event, kind, content) then
|
||||
return
|
||||
end
|
||||
local message = Message(event, kind, content)
|
||||
if not message:content():find("%s+$") then
|
||||
message:append(" ")
|
||||
end
|
||||
message:append(" ", "NoiceCursor")
|
||||
Manager.add(message)
|
||||
vim.schedule(function()
|
||||
Manager.remove(message)
|
||||
end)
|
||||
end
|
||||
|
||||
---@param entries { [1]: string, [2]: NoiceChunk[]}[]
|
||||
function M.on_history_show(event, entries)
|
||||
local contents = {}
|
||||
for _, e in pairs(entries) do
|
||||
local content = e[2]
|
||||
table.insert(contents, { 0, "\n" })
|
||||
vim.list_extend(contents, content)
|
||||
end
|
||||
local message = M.get(event)
|
||||
message:set(contents)
|
||||
Manager.add(message)
|
||||
end
|
||||
|
||||
function M.on_history_clear() end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,70 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Popupmenu = require("noice.ui.popupmenu")
|
||||
|
||||
local cmp = require("cmp")
|
||||
local cmp_config = require("cmp.config")
|
||||
|
||||
---@class NoiceCmpSource: cmp.Source
|
||||
---@field before_line string
|
||||
---@field items {label: string}[]
|
||||
local source = {}
|
||||
source.new = function()
|
||||
return setmetatable({
|
||||
items = {},
|
||||
}, { __index = source })
|
||||
end
|
||||
|
||||
function source:complete(_params, callback)
|
||||
if not Popupmenu.state.visible then
|
||||
return callback()
|
||||
end
|
||||
|
||||
local items = {}
|
||||
|
||||
for i, item in ipairs(Popupmenu.state.items) do
|
||||
table.insert(items, {
|
||||
label = item.word,
|
||||
kind = cmp.lsp.CompletionItemKind.Variable,
|
||||
preselect = i == (Popupmenu.state.selected + 1),
|
||||
})
|
||||
end
|
||||
|
||||
callback({ items = items, isIncomplete = true })
|
||||
end
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.setup()
|
||||
cmp.register_source("noice_popupmenu", source.new())
|
||||
for _, mode in ipairs({ ":" }) do
|
||||
if not cmp_config.cmdline[mode] then
|
||||
cmp.setup.cmdline(mode, {
|
||||
mapping = cmp.mapping.preset.cmdline(),
|
||||
sources = cmp.config.sources({
|
||||
{ name = "noice_popupmenu" },
|
||||
}),
|
||||
})
|
||||
cmp.core:prepare()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.on_show()
|
||||
local config = vim.deepcopy(cmp.get_config())
|
||||
config.sources = cmp.config.sources({ { name = "noice_popupmenu" } })
|
||||
cmp.core:prepare()
|
||||
cmp.complete({
|
||||
config = config,
|
||||
})
|
||||
end
|
||||
|
||||
function M.on_select()
|
||||
M.on_show()
|
||||
end
|
||||
|
||||
function M.on_hide()
|
||||
-- cmp.close()
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,98 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Util = require("noice.util")
|
||||
local Config = require("noice.config")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class PopupmenuBackend
|
||||
---@field setup fun()
|
||||
---@field on_show fun(state: Popupmenu)
|
||||
---@field on_select fun(state: Popupmenu)
|
||||
---@field on_hide fun()
|
||||
|
||||
---@class CompleteItem
|
||||
---@field word string the text that will be inserted, mandatory
|
||||
---@field abbr? string abbreviation of "word"; when not empty it is used in the menu instead of "word"
|
||||
---@field menu? string extra text for the popup menu, displayed after "word" or "abbr"
|
||||
---@field info? string more information about the item, can be displayed in a preview window
|
||||
---@field kind? string single letter indicating the type of completion
|
||||
---@field icase? boolean when non-zero case is to be ignored when comparing items to be equal; when omitted zero is used, thus items that only differ in case are added
|
||||
---@field equal? boolean when non-zero, always treat this item to be equal when comparing. Which means, "equal=1" disables filtering of this item.
|
||||
---@field dup? boolean when non-zero this match will be added even when an item with the same word is already present.
|
||||
---@field empty? boolean when non-zero this match will be added even when it is an empty string
|
||||
---@field user_data? any custom data which is associated with the item and available in |v:completed_item|; it can be any type; defaults to an empty string
|
||||
---@field text? NuiLine
|
||||
|
||||
---@class Popupmenu
|
||||
---@field selected number
|
||||
---@field col number
|
||||
---@field row number
|
||||
---@field grid number
|
||||
---@field items CompleteItem[]
|
||||
M.state = {
|
||||
visible = false,
|
||||
items = {},
|
||||
}
|
||||
|
||||
---@type PopupmenuBackend
|
||||
M.backend = nil
|
||||
|
||||
function M.setup()
|
||||
if Config.options.popupmenu.backend == "cmp" then
|
||||
M.backend = require("noice.ui.popupmenu.cmp")
|
||||
elseif Config.options.popupmenu.backend == "nui" then
|
||||
M.backend = require("noice.ui.popupmenu.nui")
|
||||
end
|
||||
M.backend.setup()
|
||||
end
|
||||
M.setup = Util.once(M.setup)
|
||||
|
||||
---@param items string[][]
|
||||
function M.on_show(_, items, selected, row, col, grid)
|
||||
local state = {
|
||||
items = vim.tbl_map(
|
||||
---@param item string[]
|
||||
function(item)
|
||||
return {
|
||||
word = item[1],
|
||||
kind = item[2],
|
||||
menu = item[3],
|
||||
info = item[4],
|
||||
}
|
||||
end,
|
||||
items
|
||||
),
|
||||
visible = true,
|
||||
selected = selected,
|
||||
row = row,
|
||||
col = col,
|
||||
grid = grid,
|
||||
}
|
||||
if not vim.deep_equal(state, M.state) then
|
||||
M.state = state
|
||||
M.setup()
|
||||
M.backend.on_show(M.state)
|
||||
end
|
||||
end
|
||||
|
||||
function M.on_select(_, selected)
|
||||
if M.state.selected ~= selected then
|
||||
M.state.selected = selected
|
||||
M.state.visible = true
|
||||
M.backend.on_select(M.state)
|
||||
end
|
||||
end
|
||||
|
||||
function M.on_hide()
|
||||
if M.state.visible then
|
||||
M.state.visible = false
|
||||
vim.schedule(function()
|
||||
if not M.state.visible then
|
||||
M.backend.on_hide()
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,270 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Util = require("noice.util")
|
||||
local Config = require("noice.config")
|
||||
local Menu = require("nui.menu")
|
||||
local Api = require("noice.api")
|
||||
local NuiLine = require("nui.line")
|
||||
local Scrollbar = require("noice.view.scrollbar")
|
||||
local Highlights = require("noice.config.highlights")
|
||||
|
||||
local M = {}
|
||||
---@type NuiMenu|NuiTree
|
||||
M.menu = nil
|
||||
|
||||
---@type NoiceScrollbar
|
||||
M.scroll = nil
|
||||
|
||||
function M.setup() end
|
||||
|
||||
---@param state Popupmenu
|
||||
function M.align(state)
|
||||
local max_width = 0
|
||||
for _, item in ipairs(state.items) do
|
||||
max_width = math.max(max_width, item.text:width())
|
||||
end
|
||||
for _, item in ipairs(state.items) do
|
||||
local width = item.text:width()
|
||||
if width < max_width then
|
||||
item.text:append(string.rep(" ", max_width - width))
|
||||
end
|
||||
end
|
||||
return max_width
|
||||
end
|
||||
|
||||
---@param item CompleteItem
|
||||
---@param prefix? string
|
||||
function M.format_abbr(item, prefix)
|
||||
local text = item.abbr or item.word
|
||||
if prefix and text:lower():find(prefix:lower(), 1, true) == 1 then
|
||||
item.text:append(text:sub(1, #prefix), "NoicePopupmenuMatch")
|
||||
item.text:append(text:sub(#prefix + 1), "NoiceCompletionItemWord")
|
||||
else
|
||||
item.text:append(text, "NoiceCompletionItemWord")
|
||||
end
|
||||
end
|
||||
|
||||
---@param item CompleteItem
|
||||
function M.format_menu(item)
|
||||
if item.menu and item.menu ~= "" then
|
||||
item.text:append(" ")
|
||||
item.text:append(item.menu, "NoiceCompletionItemMenu")
|
||||
end
|
||||
end
|
||||
|
||||
---@param item CompleteItem
|
||||
function M.format_kind(item)
|
||||
if item.kind and item.kind ~= "" then
|
||||
local hl_group = "CompletionItemKind" .. item.kind
|
||||
hl_group = Highlights.defaults[hl_group] and ("Noice" .. hl_group) or "NoiceCompletionItemKindDefault"
|
||||
local icon = Config.options.popupmenu.kind_icons[item.kind]
|
||||
item.text:append(" ")
|
||||
if icon then
|
||||
item.text:append(vim.trim(icon) .. " ", hl_group)
|
||||
end
|
||||
item.text:append(item.kind, hl_group)
|
||||
end
|
||||
end
|
||||
|
||||
---@param state Popupmenu
|
||||
function M.opts(state)
|
||||
local is_cmdline = state.grid == -1
|
||||
|
||||
local view = require("noice.config.views").get_options(is_cmdline and "cmdline_popupmenu" or "popupmenu")
|
||||
|
||||
local _opts = vim.deepcopy(view or {})
|
||||
_opts.enter = false
|
||||
_opts.type = "popup"
|
||||
|
||||
local opts = Util.nui.normalize(_opts)
|
||||
---@cast opts _.NuiPopupOptions
|
||||
|
||||
local padding = opts.border and opts.border.padding or {
|
||||
left = 0,
|
||||
right = 0,
|
||||
top = 0,
|
||||
bottom = 0,
|
||||
}
|
||||
|
||||
local position_auto = not opts.position or opts.position.col == "auto"
|
||||
if is_cmdline then
|
||||
if position_auto then
|
||||
-- Anchor to the cmdline
|
||||
local pos = Api.get_cmdline_position()
|
||||
if pos then
|
||||
opts.relative = { type = "editor" }
|
||||
opts.position = {
|
||||
row = pos.screenpos.row,
|
||||
col = pos.screenpos.col + state.col - padding.left,
|
||||
}
|
||||
-- the padding might get the window below 0
|
||||
if opts.position.col < 0 then
|
||||
opts.position.col = 0
|
||||
end
|
||||
|
||||
if pos.screenpos.row == vim.go.lines then
|
||||
opts.position.row = opts.position.row - 1
|
||||
opts.anchor = "SW"
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
opts.relative = { type = "cursor" }
|
||||
local border = vim.tbl_get(opts, "border", "style")
|
||||
local offset = (border == nil or border == "none") and 0 or 1
|
||||
opts.position = {
|
||||
row = 1 + offset,
|
||||
col = -padding.left,
|
||||
}
|
||||
end
|
||||
|
||||
-- manage left/right padding on the line
|
||||
-- otherwise the selected CursorLine does not extend to the edges
|
||||
if opts.border and opts.border.padding then
|
||||
opts.border.padding = vim.tbl_deep_extend("force", {}, padding, { left = 0, right = 0 })
|
||||
if opts.size and type(opts.size.width) == "number" then
|
||||
opts.size.width = opts.size.width + padding.left + padding.right
|
||||
end
|
||||
end
|
||||
|
||||
return opts, padding
|
||||
end
|
||||
|
||||
---@param state Popupmenu
|
||||
function M.show(state)
|
||||
-- M.on_hide()
|
||||
local is_cmdline = state.grid == -1
|
||||
local opts, padding = M.opts(state)
|
||||
|
||||
---@type string?
|
||||
local prefix = nil
|
||||
|
||||
if is_cmdline then
|
||||
prefix = vim.fn.getcmdline():sub(state.col + 1, vim.fn.getcmdpos() - 1)
|
||||
elseif #state.items > 0 then
|
||||
prefix = state.items[1].word
|
||||
for _, item in ipairs(state.items) do
|
||||
for i = 1, #prefix do
|
||||
if prefix:sub(i, i) ~= item.word:sub(i, i) then
|
||||
prefix = prefix:sub(1, i - 1)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for _, item in ipairs(state.items) do
|
||||
if type(item) == "string" then
|
||||
item = { word = item }
|
||||
end
|
||||
item.text = NuiLine()
|
||||
if padding.left then
|
||||
item.text:append(string.rep(" ", padding.left))
|
||||
end
|
||||
end
|
||||
|
||||
local max_width = 0
|
||||
for _, format in ipairs({ M.format_abbr, M.format_menu, M.format_kind }) do
|
||||
for _, item in ipairs(state.items) do
|
||||
format(item, prefix)
|
||||
end
|
||||
max_width = M.align(state)
|
||||
end
|
||||
|
||||
for _, item in ipairs(state.items) do
|
||||
if padding.right then
|
||||
item.text:append(string.rep(" ", padding.right))
|
||||
end
|
||||
for _, t in ipairs(item.text._texts) do
|
||||
t._content = t._content:gsub("[\n\r]+", " ")
|
||||
end
|
||||
end
|
||||
|
||||
opts = vim.tbl_deep_extend(
|
||||
"force",
|
||||
opts,
|
||||
Util.nui.get_layout({
|
||||
width = max_width + 1, -- +1 for scrollbar
|
||||
height = #state.items,
|
||||
}, opts)
|
||||
)
|
||||
|
||||
---@type NuiTreeNode[]
|
||||
local items = vim.tbl_map(function(item)
|
||||
return Menu.item(item)
|
||||
end, state.items)
|
||||
for i, item in ipairs(items) do
|
||||
item._index = i
|
||||
end
|
||||
|
||||
if M.menu then
|
||||
M.menu._.items = items
|
||||
M.menu.tree:set_nodes(items)
|
||||
M.menu.tree:render()
|
||||
M.menu:update_layout(opts)
|
||||
else
|
||||
M.create(items, opts)
|
||||
end
|
||||
|
||||
-- redraw is needed when in blocking mode
|
||||
if Util.is_blocking() then
|
||||
Util.redraw()
|
||||
end
|
||||
|
||||
M.on_select(state)
|
||||
end
|
||||
|
||||
---@param opts _.NuiPopupOptions
|
||||
---@param items NuiTreeNode[]
|
||||
function M.create(items, opts)
|
||||
M.menu = Menu(opts, {
|
||||
lines = items,
|
||||
})
|
||||
M.menu:mount()
|
||||
Util.tag(M.menu.bufnr, "popupmenu")
|
||||
if M.menu.border then
|
||||
Util.tag(M.menu.border.bufnr, "popupmenu.border")
|
||||
end
|
||||
|
||||
M.scroll = Scrollbar({
|
||||
winnr = M.menu.winid,
|
||||
padding = Util.nui.normalize_padding(opts.border),
|
||||
})
|
||||
M.scroll:mount()
|
||||
end
|
||||
|
||||
---@param state Popupmenu
|
||||
function M.on_show(state)
|
||||
M.show(state)
|
||||
end
|
||||
|
||||
---@param state Popupmenu
|
||||
function M.on_select(state)
|
||||
if M.menu and state.selected ~= -1 then
|
||||
vim.api.nvim_win_set_cursor(M.menu.winid, { state.selected + 1, 0 })
|
||||
vim.api.nvim_exec_autocmds("WinScrolled", { modeline = false })
|
||||
end
|
||||
end
|
||||
|
||||
function M.on_hide()
|
||||
Util.protect(function()
|
||||
if M.menu then
|
||||
M.menu:unmount()
|
||||
M.menu = nil
|
||||
end
|
||||
if M.scroll then
|
||||
M.scroll:unmount()
|
||||
M.scroll = nil
|
||||
end
|
||||
end, {
|
||||
finally = function()
|
||||
if M.menu then
|
||||
M.menu._.loading = false
|
||||
end
|
||||
end,
|
||||
retry_on_E11 = true,
|
||||
retry_on_E565 = true,
|
||||
})()
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,27 @@
|
||||
local M = {}
|
||||
|
||||
---@type table<string, any>
|
||||
M.state = {}
|
||||
|
||||
function M.set(event, ...)
|
||||
local msg = { event, ... }
|
||||
M.state[event] = msg
|
||||
end
|
||||
|
||||
function M.clear(event)
|
||||
M.state[event] = nil
|
||||
end
|
||||
|
||||
function M.is_equal(event, ...)
|
||||
local msg = { event, ... }
|
||||
return vim.deep_equal(M.state[event], msg)
|
||||
end
|
||||
|
||||
function M.skip(event, ...)
|
||||
if M.is_equal(event, ...) then
|
||||
return true
|
||||
end
|
||||
M.set(event, ...)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,168 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Config = require("noice.config")
|
||||
local Util = require("noice.util")
|
||||
|
||||
---@class CallOptions
|
||||
---@field catch? fun(err:string)
|
||||
---@field finally? fun()
|
||||
---@field msg? string
|
||||
---@field retry_on_vim_errors? boolean
|
||||
---@field retry_on_E11? boolean Retry on errors due to illegal operations while the cmdline window is open
|
||||
local defaults = {
|
||||
retry_on_vim_errors = true,
|
||||
retry_on_E11 = false,
|
||||
ignore_E565 = true,
|
||||
retry_on_E565 = false,
|
||||
ignore_keyboard_interrupt = true,
|
||||
}
|
||||
|
||||
---@class Call
|
||||
---@field _fn fun()
|
||||
---@field _opts CallOptions
|
||||
---@field _retry boolean
|
||||
---@field _defer_retry boolean
|
||||
local M = {}
|
||||
M.__index = M
|
||||
|
||||
M._errors = 0
|
||||
M._max_errors = 20
|
||||
|
||||
function M.reset()
|
||||
M.reset = Util.debounce(200, function()
|
||||
M._errors = 0
|
||||
end)
|
||||
M.reset()
|
||||
end
|
||||
|
||||
---@generic F: fun()
|
||||
---@param fn F
|
||||
---@param opts? CallOptions
|
||||
---@return F
|
||||
function M.protect(fn, opts)
|
||||
if not fn then
|
||||
local trace = debug.traceback()
|
||||
Util.panic("nil passed to noice.util.call.protect. This should not happen.\n%s", trace)
|
||||
end
|
||||
local self = setmetatable({}, M)
|
||||
self._opts = vim.tbl_deep_extend("force", defaults, opts or {})
|
||||
self._fn = fn
|
||||
self._retry = false
|
||||
return function(...)
|
||||
return self(...)
|
||||
end
|
||||
end
|
||||
|
||||
function M:on_error(err)
|
||||
M._errors = M._errors + 1
|
||||
if M._errors > M._max_errors then
|
||||
Util.panic("Too many errors. Disabling Noice")
|
||||
end
|
||||
M.reset()
|
||||
|
||||
if self._opts.catch then
|
||||
pcall(self._opts.catch, err)
|
||||
end
|
||||
|
||||
if err then
|
||||
if self._opts.ignore_keyboard_interrupt and err:lower():find("keyboard interrupt") then
|
||||
M._errors = M._errors - 1
|
||||
return
|
||||
end
|
||||
|
||||
if self._opts.retry_on_E565 and err:find("E565") then
|
||||
M._errors = M._errors - 1
|
||||
self._defer_retry = true
|
||||
return
|
||||
end
|
||||
|
||||
if self._opts.ignore_E565 and err:find("E565") then
|
||||
M._errors = M._errors - 1
|
||||
return
|
||||
end
|
||||
|
||||
-- catch any Vim Errors and retry once
|
||||
if not self._retry and err:find("Vim:E%d+") and self._opts.retry_on_vim_errors then
|
||||
self._retry = true
|
||||
return
|
||||
end
|
||||
|
||||
if self._opts.retry_on_E11 and err:find("E11:") then
|
||||
M._errors = M._errors - 1
|
||||
self._defer_retry = true
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
pcall(M.log, self, err)
|
||||
self:notify(err)
|
||||
end
|
||||
|
||||
function M:log(data)
|
||||
local file = Config.options.log
|
||||
local fd = io.open(file, "a+")
|
||||
if not fd then
|
||||
error(("Could not open file %s for writing"):format(file))
|
||||
end
|
||||
fd:write("\n\n" .. os.date() .. "\n" .. self:format(data, true))
|
||||
fd:close()
|
||||
end
|
||||
|
||||
function M:format(err, stack)
|
||||
local lines = {}
|
||||
table.insert(lines, self._opts.msg or err)
|
||||
|
||||
if stack then
|
||||
if self._opts.msg then
|
||||
table.insert(lines, err)
|
||||
end
|
||||
table.insert(lines, debug.traceback("", 5))
|
||||
end
|
||||
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
function M:notify(err)
|
||||
local msg = self:format(err, Config.options.debug)
|
||||
vim.schedule(function()
|
||||
if not pcall(Util.error, msg) then
|
||||
vim.notify(msg, vim.log.levels.ERROR, { title = "noice.nvim" })
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function M:__call(...)
|
||||
local args = vim.F.pack_len(...)
|
||||
|
||||
-- wrap the function and call with args
|
||||
local wrapped = function()
|
||||
return self._fn(vim.F.unpack_len(args))
|
||||
end
|
||||
|
||||
-- error handler
|
||||
local error_handler = function(err)
|
||||
pcall(self.on_error, self, err)
|
||||
return err
|
||||
end
|
||||
|
||||
---@type boolean, any
|
||||
local ok, result = xpcall(wrapped, error_handler)
|
||||
if self._retry then
|
||||
---@type boolean, any
|
||||
ok, result = xpcall(wrapped, error_handler)
|
||||
end
|
||||
|
||||
if self._opts.finally then
|
||||
pcall(self._opts.finally)
|
||||
end
|
||||
|
||||
if not ok and self._defer_retry then
|
||||
vim.defer_fn(function()
|
||||
self(vim.F.unpack_len(args))
|
||||
end, 100)
|
||||
end
|
||||
|
||||
return ok and result or nil
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,40 @@
|
||||
local M = {}
|
||||
|
||||
---@return ffi.namespace*
|
||||
function M.load()
|
||||
-- Put in a global var to make sure we dont reload
|
||||
-- when plugin reloaders do their thing
|
||||
if not _G.noice_C then
|
||||
local ffi = require("ffi")
|
||||
local ok, err = pcall(
|
||||
ffi.cdef,
|
||||
[[typedef int32_t RgbValue;
|
||||
typedef struct attr_entry {
|
||||
int16_t rgb_ae_attr, cterm_ae_attr;
|
||||
RgbValue rgb_fg_color, rgb_bg_color, rgb_sp_color;
|
||||
int cterm_fg_color, cterm_bg_color;
|
||||
int hl_blend;
|
||||
} HlAttrs;
|
||||
HlAttrs syn_attr2entry(int attr);
|
||||
bool cmdpreview;
|
||||
extern int textlock;
|
||||
void setcursor_mayforce(bool force);
|
||||
]]
|
||||
)
|
||||
---@diagnostic disable-next-line: need-check-nil
|
||||
if not ok then
|
||||
error(err)
|
||||
end
|
||||
_G.noice_C = ffi.C
|
||||
end
|
||||
return _G.noice_C
|
||||
end
|
||||
|
||||
return setmetatable(M, {
|
||||
__index = function(_, key)
|
||||
return M.load()[key]
|
||||
end,
|
||||
__newindex = function(_, k, v)
|
||||
M.load()[k] = v
|
||||
end,
|
||||
})
|
||||
@ -0,0 +1,317 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Util = require("noice.util")
|
||||
local Router = require("noice.message.router")
|
||||
local Api = require("noice.api")
|
||||
local Cmdline = require("noice.ui.cmdline")
|
||||
|
||||
-- HACK: a bunch of hacks to make Noice behave
|
||||
local M = {}
|
||||
|
||||
---@type fun()[]
|
||||
M._disable = {}
|
||||
|
||||
function M.reset_augroup()
|
||||
M.group = vim.api.nvim_create_augroup("noice.hacks", { clear = true })
|
||||
end
|
||||
|
||||
function M.enable()
|
||||
M.reset_augroup()
|
||||
M.fix_input()
|
||||
M.fix_redraw()
|
||||
M.fix_cmp()
|
||||
M.fix_vim_sleuth()
|
||||
-- M.fix_cmdpreview()
|
||||
|
||||
-- Hacks for Neovim < 0.10
|
||||
if vim.fn.has("nvim-0.10") == 0 then
|
||||
M.fix_incsearch()
|
||||
end
|
||||
end
|
||||
|
||||
function M.fix_vim_sleuth()
|
||||
vim.g.sleuth_noice_heuristics = 0
|
||||
end
|
||||
|
||||
function M.disable()
|
||||
M.reset_augroup()
|
||||
for _, fn in pairs(M._disable) do
|
||||
fn()
|
||||
end
|
||||
M._disable = {}
|
||||
end
|
||||
|
||||
-- start a timer that checks for vim.v.hlsearch.
|
||||
-- Clears search count and stops timer when hlsearch==0
|
||||
function M.fix_nohlsearch()
|
||||
M.fix_nohlsearch = Util.interval(30, function()
|
||||
if vim.o.hlsearch and vim.v.hlsearch == 0 then
|
||||
local m = require("noice.ui.msg").get("msg_show", "search_count")
|
||||
require("noice.message.manager").remove(m)
|
||||
end
|
||||
end, {
|
||||
enabled = function()
|
||||
return vim.o.hlsearch and vim.v.hlsearch == 1
|
||||
end,
|
||||
})
|
||||
M.fix_nohlsearch()
|
||||
end
|
||||
|
||||
---@see https://github.com/neovim/neovim/issues/20793
|
||||
function M.draw_cursor()
|
||||
if vim.api.nvim__redraw then
|
||||
vim.api.nvim__redraw({ cursor = true })
|
||||
else
|
||||
require("noice.util.ffi").setcursor_mayforce(true)
|
||||
end
|
||||
end
|
||||
|
||||
---@see https://github.com/neovim/neovim/issues/17810
|
||||
function M.fix_incsearch()
|
||||
---@type integer|nil
|
||||
local conceallevel
|
||||
|
||||
vim.api.nvim_create_autocmd("CmdlineEnter", {
|
||||
group = M.group,
|
||||
callback = function(event)
|
||||
if event.match == "/" or event.match == "?" then
|
||||
conceallevel = vim.wo.conceallevel
|
||||
vim.opt_local.conceallevel = 0
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
vim.api.nvim_create_autocmd("CmdlineLeave", {
|
||||
group = M.group,
|
||||
callback = function(event)
|
||||
if conceallevel and (event.match == "/" or event.match == "?") then
|
||||
vim.opt_local.conceallevel = conceallevel
|
||||
conceallevel = nil
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
-- we need to intercept redraw so we can safely ignore message triggered by redraw
|
||||
-- This wraps vim.cmd, nvim_cmd, nvim_command and nvim_exec
|
||||
---@see https://github.com/neovim/neovim/issues/20416
|
||||
M.inside_redraw = false
|
||||
function M.fix_redraw()
|
||||
local nvim_cmd = vim.api.nvim_cmd
|
||||
|
||||
local function wrap(fn, ...)
|
||||
local inside_redraw = M.inside_redraw
|
||||
|
||||
M.inside_redraw = true
|
||||
|
||||
---@type boolean, any
|
||||
local ok, ret = pcall(fn, ...)
|
||||
|
||||
-- check if the ui needs updating
|
||||
Util.try(Router.update)
|
||||
|
||||
if not inside_redraw then
|
||||
M.inside_redraw = false
|
||||
end
|
||||
|
||||
if ok then
|
||||
return ret
|
||||
end
|
||||
error(ret)
|
||||
end
|
||||
|
||||
vim.api.nvim_cmd = function(cmd, ...)
|
||||
if type(cmd) == "table" and cmd.cmd and cmd.cmd == "redraw" then
|
||||
return wrap(nvim_cmd, cmd, ...)
|
||||
else
|
||||
return nvim_cmd(cmd, ...)
|
||||
end
|
||||
end
|
||||
|
||||
local nvim_command = vim.api.nvim_command
|
||||
vim.api.nvim_command = function(cmd, ...)
|
||||
if cmd == "redraw" then
|
||||
return wrap(nvim_command, cmd, ...)
|
||||
else
|
||||
return nvim_command(cmd, ...)
|
||||
end
|
||||
end
|
||||
|
||||
local nvim_exec = vim.api.nvim_exec
|
||||
vim.api.nvim_exec = function(cmd, ...)
|
||||
if type(cmd) == "string" and cmd:find("redraw") then
|
||||
-- WARN: this will potentially lose messages before or after the redraw ex command
|
||||
-- example: echo "foo" | redraw | echo "bar"
|
||||
-- the 'foo' message will be lost
|
||||
return wrap(nvim_exec, cmd, ...)
|
||||
else
|
||||
return nvim_exec(cmd, ...)
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(M._disable, function()
|
||||
vim.api.nvim_cmd = nvim_cmd
|
||||
vim.api.nvim_command = nvim_command
|
||||
vim.api.nvim_exec = nvim_exec
|
||||
end)
|
||||
end
|
||||
|
||||
---@see https://github.com/neovim/neovim/issues/20311
|
||||
M.before_input = false
|
||||
function M.fix_input()
|
||||
local function wrap(fn, skip)
|
||||
return function(...)
|
||||
if skip and skip(...) then
|
||||
return fn(...)
|
||||
end
|
||||
|
||||
-- make sure the cursor is drawn before blocking
|
||||
M.draw_cursor()
|
||||
|
||||
local Manager = require("noice.message.manager")
|
||||
|
||||
-- do any updates now before blocking
|
||||
M.before_input = true
|
||||
Router.update()
|
||||
|
||||
---@type boolean, any
|
||||
local ok, ret = pcall(fn, ...)
|
||||
|
||||
-- clear any message right after input
|
||||
Manager.clear({ event = "msg_show", kind = { "echo", "echomsg", "" } })
|
||||
|
||||
M.before_input = false
|
||||
if ok then
|
||||
return ret
|
||||
end
|
||||
error(ret)
|
||||
end
|
||||
end
|
||||
|
||||
local function skip(expr)
|
||||
return expr ~= nil
|
||||
end
|
||||
local getchar = vim.fn.getchar
|
||||
local getcharstr = vim.fn.getcharstr
|
||||
local inputlist = vim.fn.inputlist
|
||||
-- local confirm = vim.fn.confirm
|
||||
|
||||
vim.fn.getchar = wrap(vim.fn.getchar, skip)
|
||||
vim.fn.getcharstr = wrap(vim.fn.getcharstr, skip)
|
||||
vim.fn.inputlist = wrap(vim.fn.inputlist, nil)
|
||||
-- vim.fn.confirm = wrap(vim.fn.confirm, nil)
|
||||
|
||||
table.insert(M._disable, function()
|
||||
vim.fn.getchar = getchar
|
||||
vim.fn.getcharstr = getcharstr
|
||||
vim.fn.inputlist = inputlist
|
||||
-- vim.fn.confirm = confirm
|
||||
end)
|
||||
end
|
||||
|
||||
-- Fixes cmp cmdline position
|
||||
function M.fix_cmp()
|
||||
M.on_module("cmp.utils.api", function(api)
|
||||
local get_cursor = api.get_cursor
|
||||
api.get_cursor = function()
|
||||
if api.is_cmdline_mode() then
|
||||
local pos = Api.get_cmdline_position()
|
||||
if pos then
|
||||
return { pos.bufpos.row, vim.fn.getcmdpos() - 1 }
|
||||
end
|
||||
end
|
||||
return get_cursor()
|
||||
end
|
||||
|
||||
local get_screen_cursor = api.get_screen_cursor
|
||||
api.get_screen_cursor = function()
|
||||
if api.is_cmdline_mode() then
|
||||
local pos = Api.get_cmdline_position()
|
||||
if pos then
|
||||
local col = vim.fn.getcmdpos() - Cmdline.last().offset
|
||||
return { pos.screenpos.row, pos.screenpos.col + col }
|
||||
end
|
||||
end
|
||||
return get_screen_cursor()
|
||||
end
|
||||
|
||||
table.insert(M._disable, function()
|
||||
api.get_cursor = get_cursor
|
||||
api.get_screen_cursor = get_screen_cursor
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
function M.fix_cmdpreview()
|
||||
vim.api.nvim_create_autocmd("CmdlineChanged", {
|
||||
group = M.group,
|
||||
callback = function()
|
||||
local ffi = require("noice.util.ffi")
|
||||
ffi.cmdpreview = false
|
||||
vim.cmd([[redraw]])
|
||||
Util.try(require("noice.message.router").update)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
M.SPECIAL = "Þ"
|
||||
function M.cmdline_force_redraw()
|
||||
if not require("noice.util.ffi").cmdpreview then
|
||||
return
|
||||
end
|
||||
|
||||
-- HACK: this will trigger redraw during substitute and cmdpreview
|
||||
vim.api.nvim_feedkeys(M.SPECIAL .. Util.BS, "n", true)
|
||||
end
|
||||
|
||||
---@type string?
|
||||
M._guicursor = nil
|
||||
function M.hide_cursor()
|
||||
if M._guicursor == nil then
|
||||
M._guicursor = vim.go.guicursor
|
||||
end
|
||||
-- schedule this, since otherwise Neovide crashes
|
||||
vim.schedule(function()
|
||||
if M._guicursor then
|
||||
vim.go.guicursor = "a:NoiceHiddenCursor"
|
||||
end
|
||||
end)
|
||||
M._disable.guicursor = M.show_cursor
|
||||
end
|
||||
|
||||
function M.show_cursor()
|
||||
if M._guicursor then
|
||||
if not Util.is_exiting() then
|
||||
vim.schedule(function()
|
||||
if M._guicursor and not Util.is_exiting() then
|
||||
-- we need to reset all first and then wait for some time before resetting the guicursor. See #114
|
||||
vim.go.guicursor = "a:"
|
||||
vim.cmd.redrawstatus()
|
||||
vim.go.guicursor = M._guicursor
|
||||
M._guicursor = nil
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param fn fun(mod)
|
||||
function M.on_module(module, fn)
|
||||
if package.loaded[module] then
|
||||
return fn(package.loaded[module])
|
||||
end
|
||||
|
||||
package.preload[module] = function()
|
||||
package.preload[module] = nil
|
||||
for _, loader in pairs(package.loaders) do
|
||||
local ret = loader(module)
|
||||
if type(ret) == "function" then
|
||||
local mod = ret()
|
||||
fn(mod)
|
||||
return mod
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,383 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Hacks = require("noice.util.hacks")
|
||||
local Config = require("noice.config")
|
||||
|
||||
local M = {}
|
||||
|
||||
M.islist = vim.islist or vim.tbl_islist
|
||||
|
||||
M.stats = require("noice.util.stats")
|
||||
M.call = require("noice.util.call")
|
||||
M.nui = require("noice.util.nui")
|
||||
|
||||
function M.t(str)
|
||||
return vim.api.nvim_replace_termcodes(str, true, true, true)
|
||||
end
|
||||
|
||||
M.CR = M.t("<cr>")
|
||||
M.ESC = M.t("<esc>")
|
||||
M.BS = M.t("<bs>")
|
||||
M.EXIT = M.t("<C-\\><C-n>")
|
||||
M.LUA_CALLBACK = "\x80\253g"
|
||||
M.RIGHT = M.t("<right>")
|
||||
M.LEFT = M.t("<left>")
|
||||
M.CMD = "\x80\253h"
|
||||
|
||||
---@generic F: fun()
|
||||
---@param fn F
|
||||
---@return F
|
||||
function M.once(fn)
|
||||
local done = false
|
||||
return function(...)
|
||||
if not done then
|
||||
fn(...)
|
||||
done = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.open(uri)
|
||||
local cmd
|
||||
if vim.fn.has("win32") == 1 then
|
||||
cmd = { "cmd.exe", "/c", "start", '""', vim.fn.shellescape(uri) }
|
||||
elseif vim.fn.has("macunix") == 1 then
|
||||
cmd = { "open", uri }
|
||||
else
|
||||
cmd = { "xdg-open", uri }
|
||||
end
|
||||
|
||||
local ret = vim.fn.system(cmd)
|
||||
if vim.v.shell_error ~= 0 then
|
||||
local msg = {
|
||||
"Failed to open uri",
|
||||
ret,
|
||||
vim.inspect(cmd),
|
||||
}
|
||||
vim.notify(table.concat(msg, "\n"), vim.log.levels.ERROR)
|
||||
end
|
||||
end
|
||||
|
||||
---@param fn fun():any
|
||||
function M.ignore_events(fn)
|
||||
local ei = vim.go.eventignore
|
||||
vim.go.eventignore = "all"
|
||||
local ret = fn()
|
||||
vim.go.eventignore = ei
|
||||
return ret
|
||||
end
|
||||
|
||||
function M.tag(buf, tag)
|
||||
local ft = vim.bo[buf].filetype
|
||||
|
||||
if ft == "" then
|
||||
M.ignore_events(function()
|
||||
vim.bo[buf].filetype = "noice"
|
||||
end)
|
||||
end
|
||||
|
||||
if Config.options.debug and vim.api.nvim_buf_get_name(buf) == "" then
|
||||
local path = "noice://" .. buf .. "/" .. tag
|
||||
local params = {}
|
||||
if ft ~= "" and ft ~= "noice" then
|
||||
table.insert(params, "filetype=" .. ft)
|
||||
end
|
||||
if #params > 0 then
|
||||
path = path .. "?" .. table.concat(params, "&")
|
||||
end
|
||||
vim.api.nvim_buf_set_name(buf, path)
|
||||
end
|
||||
end
|
||||
|
||||
---@param win window
|
||||
---@param options table<string, any>
|
||||
function M.wo(win, options)
|
||||
for k, v in pairs(options) do
|
||||
if vim.api.nvim_set_option_value then
|
||||
vim.api.nvim_set_option_value(k, v, { scope = "local", win = win })
|
||||
else
|
||||
vim.wo[win][k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.debounce(ms, fn)
|
||||
local timer = vim.loop.new_timer()
|
||||
return function(...)
|
||||
local argv = vim.F.pack_len(...)
|
||||
timer:start(ms, 0, function()
|
||||
timer:stop()
|
||||
vim.schedule_wrap(fn)(vim.F.unpack_len(argv))
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
---@generic F: fun()
|
||||
---@param fn F
|
||||
---@param ms integer
|
||||
---@param opts? {enabled?:fun():boolean}
|
||||
---@return F|Interval
|
||||
function M.interval(ms, fn, opts)
|
||||
opts = opts or {}
|
||||
---@type vim.loop.Timer?
|
||||
local timer = nil
|
||||
|
||||
---@class Interval
|
||||
local T = {}
|
||||
|
||||
function T.keep()
|
||||
if M.is_exiting() then
|
||||
return false
|
||||
end
|
||||
return opts.enabled == nil or opts.enabled()
|
||||
end
|
||||
|
||||
function T.running()
|
||||
return timer and not timer:is_closing()
|
||||
end
|
||||
|
||||
function T.stop()
|
||||
if timer and T.running() then
|
||||
timer:stop()
|
||||
end
|
||||
end
|
||||
|
||||
function T.fn()
|
||||
pcall(fn)
|
||||
if timer and T.running() and not T.keep() then
|
||||
timer:stop()
|
||||
elseif T.keep() and not T.running() then
|
||||
timer = vim.defer_fn(T.fn, ms)
|
||||
end
|
||||
end
|
||||
|
||||
function T.__call()
|
||||
if not T.running() and T.keep() then
|
||||
timer = vim.defer_fn(T.fn, ms)
|
||||
end
|
||||
end
|
||||
|
||||
return setmetatable(T, T)
|
||||
end
|
||||
|
||||
---@param a table<string, any>
|
||||
---@param b table<string, any>
|
||||
---@return string[]
|
||||
function M.diff_keys(a, b)
|
||||
local diff = {}
|
||||
for k, _ in pairs(a) do
|
||||
if not vim.deep_equal(a[k], b[k]) then
|
||||
diff[k] = true
|
||||
end
|
||||
end
|
||||
for k, _ in pairs(b) do
|
||||
if not vim.deep_equal(a[k], b[k]) then
|
||||
diff[k] = true
|
||||
end
|
||||
end
|
||||
return vim.tbl_keys(diff)
|
||||
end
|
||||
|
||||
function M.module_exists(mod)
|
||||
return pcall(_G.require, mod) == true
|
||||
end
|
||||
|
||||
function M.diff(a, b)
|
||||
a = vim.deepcopy(a)
|
||||
b = vim.deepcopy(b)
|
||||
M._diff(a, b)
|
||||
return { left = a, right = b }
|
||||
end
|
||||
|
||||
function M._diff(a, b)
|
||||
if a == b then
|
||||
return true
|
||||
end
|
||||
if type(a) ~= type(b) then
|
||||
return false
|
||||
end
|
||||
if type(a) == "table" then
|
||||
local equal = true
|
||||
for k, v in pairs(a) do
|
||||
if M._diff(v, b[k]) then
|
||||
a[k] = nil
|
||||
b[k] = nil
|
||||
else
|
||||
equal = false
|
||||
end
|
||||
end
|
||||
for k, _ in pairs(b) do
|
||||
if a[k] == nil then
|
||||
equal = false
|
||||
break
|
||||
end
|
||||
end
|
||||
return equal
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---@param opts? {blocking:boolean, mode:boolean, input:boolean, redraw:boolean}
|
||||
function M.is_blocking(opts)
|
||||
opts = vim.tbl_deep_extend("force", {
|
||||
blocking = true,
|
||||
mode = true,
|
||||
input = true,
|
||||
redraw = true,
|
||||
}, opts or {})
|
||||
local mode = vim.api.nvim_get_mode()
|
||||
|
||||
local blocking_mode = false
|
||||
for _, m in ipairs({ "ic", "ix", "c", "no", "r%?", "rm" }) do
|
||||
if mode.mode:find(m) == 1 then
|
||||
blocking_mode = true
|
||||
end
|
||||
end
|
||||
|
||||
local reason = opts.blocking and mode.blocking and "blocking"
|
||||
or opts.mode and blocking_mode and ("mode:" .. mode.mode)
|
||||
or opts.input and Hacks.before_input and "input"
|
||||
or opts.redraw and Hacks.inside_redraw and "redraw"
|
||||
or #require("noice.ui.cmdline").cmdlines > 0 and "cmdline"
|
||||
or nil
|
||||
return reason ~= nil, reason
|
||||
end
|
||||
|
||||
function M.redraw()
|
||||
vim.cmd.redraw()
|
||||
M.stats.track("redraw")
|
||||
end
|
||||
|
||||
M.protect = M.call.protect
|
||||
|
||||
function M.try(fn, ...)
|
||||
return M.call.protect(fn)(...)
|
||||
end
|
||||
|
||||
function M.win_apply_config(win, opts)
|
||||
opts = vim.tbl_deep_extend("force", vim.api.nvim_win_get_config(win), opts or {})
|
||||
vim.api.nvim_win_set_config(win, opts)
|
||||
end
|
||||
|
||||
---@param msg string
|
||||
---@param level number
|
||||
---@param ... any
|
||||
function M.notify(msg, level, ...)
|
||||
if M.module_exists("notify") then
|
||||
require("notify").notify(msg:format(...), level, {
|
||||
title = "noice.nvim",
|
||||
on_open = function(win)
|
||||
vim.api.nvim_win_set_option(win, "conceallevel", 3)
|
||||
local buf = vim.api.nvim_win_get_buf(win)
|
||||
vim.api.nvim_buf_set_option(buf, "filetype", "markdown")
|
||||
vim.api.nvim_win_set_option(win, "spell", false)
|
||||
end,
|
||||
})
|
||||
else
|
||||
vim.notify(msg:format(...), level, {
|
||||
title = "noice.nvim",
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
---@type table<string, boolean>
|
||||
M._once = {}
|
||||
|
||||
---@param msg string
|
||||
---@param level number
|
||||
---@param ... any
|
||||
function M.notify_once(msg, level, ...)
|
||||
msg = msg:format(...)
|
||||
local once = level .. msg
|
||||
if not M._once[once] then
|
||||
M.notify(msg, level)
|
||||
M._once[once] = true
|
||||
end
|
||||
end
|
||||
|
||||
function M.warn_once(msg, ...)
|
||||
M.notify_once(msg, vim.log.levels.WARN, ...)
|
||||
end
|
||||
|
||||
function M.error_once(msg, ...)
|
||||
M.notify_once(msg, vim.log.levels.ERROR, ...)
|
||||
end
|
||||
|
||||
function M.warn(msg, ...)
|
||||
M.notify(msg, vim.log.levels.WARN, ...)
|
||||
end
|
||||
|
||||
function M.error(msg, ...)
|
||||
M.notify(msg, vim.log.levels.ERROR, ...)
|
||||
end
|
||||
|
||||
--- Will stop Noice and show error
|
||||
function M.panic(msg, ...)
|
||||
require("noice").disable()
|
||||
require("noice.view.backend.notify").dismiss()
|
||||
vim.notify(msg:format(...), vim.log.levels.ERROR)
|
||||
error("Noice was stopped to prevent further errors", 0)
|
||||
end
|
||||
|
||||
function M.info(msg, ...)
|
||||
M.notify(msg, vim.log.levels.INFO, ...)
|
||||
end
|
||||
|
||||
---@param data any
|
||||
function M.debug(data)
|
||||
if not Config.options.debug then
|
||||
return
|
||||
end
|
||||
if type(data) == "function" then
|
||||
data = data()
|
||||
end
|
||||
if type(data) ~= "string" then
|
||||
data = vim.inspect(data)
|
||||
end
|
||||
local file = "./noice.log"
|
||||
local fd = io.open(file, "a+")
|
||||
if not fd then
|
||||
error(("Could not open file %s for writing"):format(file))
|
||||
end
|
||||
fd:write(data .. "\n")
|
||||
fd:close()
|
||||
end
|
||||
|
||||
---@return string
|
||||
function M.read_file(file)
|
||||
local fd = io.open(file, "r")
|
||||
if not fd then
|
||||
error(("Could not open file %s for reading"):format(file))
|
||||
end
|
||||
local data = fd:read("*a")
|
||||
fd:close()
|
||||
return data
|
||||
end
|
||||
|
||||
function M.is_exiting()
|
||||
return vim.v.exiting ~= vim.NIL
|
||||
end
|
||||
|
||||
function M.write_file(file, data)
|
||||
local fd = io.open(file, "w+")
|
||||
if not fd then
|
||||
error(("Could not open file %s for writing"):format(file))
|
||||
end
|
||||
fd:write(data)
|
||||
fd:close()
|
||||
end
|
||||
|
||||
---@generic K
|
||||
---@generic V
|
||||
---@param tbl table<K, V>
|
||||
---@param fn fun(key: K, value: V)
|
||||
---@param sorter? fun(a:V, b:V): boolean
|
||||
function M.for_each(tbl, fn, sorter)
|
||||
local keys = vim.tbl_keys(tbl)
|
||||
table.sort(keys, sorter)
|
||||
for _, key in ipairs(keys) do
|
||||
fn(key, tbl[key])
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,25 @@
|
||||
return function(module)
|
||||
local mod = nil
|
||||
|
||||
local function load()
|
||||
if not mod then
|
||||
mod = require(module)
|
||||
package.loaded[module] = mod
|
||||
end
|
||||
return mod
|
||||
end
|
||||
-- if already loaded, return the module
|
||||
-- otherwise return a lazy module
|
||||
return type(package.loaded[module]) == "table" and package.loaded[module]
|
||||
or setmetatable({}, {
|
||||
__index = function(_, key)
|
||||
return load()[key]
|
||||
end,
|
||||
__newindex = function(_, key, value)
|
||||
load()[key] = value
|
||||
end,
|
||||
__call = function(_, ...)
|
||||
return load()(...)
|
||||
end,
|
||||
})
|
||||
end
|
||||
@ -0,0 +1,254 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Util = require("noice.util")
|
||||
local _ = require("nui.utils")._
|
||||
|
||||
local M = {}
|
||||
|
||||
---@param opts? NoiceNuiOptions
|
||||
---@return _.NoiceNuiOptions
|
||||
function M.normalize(opts)
|
||||
opts = opts or {}
|
||||
|
||||
M.normalize_win_options(opts)
|
||||
|
||||
if opts.type == "split" then
|
||||
---@cast opts NuiSplitOptions
|
||||
return M.normalize_split_options(opts)
|
||||
elseif opts.type == "popup" then
|
||||
---@cast opts NuiPopupOptions
|
||||
return M.normalize_popup_options(opts)
|
||||
end
|
||||
error("Missing type for " .. vim.inspect(opts))
|
||||
end
|
||||
|
||||
---@param opts? NoiceNuiOptions
|
||||
function M.normalize_win_options(opts)
|
||||
opts = opts or {}
|
||||
if opts.win_options and opts.win_options.winhighlight then
|
||||
opts.win_options.winhighlight = Util.nui.get_win_highlight(opts.win_options.winhighlight)
|
||||
end
|
||||
end
|
||||
|
||||
---@return {xmin:integer, xmax:integer, ymin:integer, ymax:integer}
|
||||
function M.bounds(win)
|
||||
local pos = vim.api.nvim_win_get_position(win)
|
||||
local height = vim.api.nvim_win_get_height(win)
|
||||
local width = vim.api.nvim_win_get_width(win)
|
||||
return {
|
||||
xmin = pos[2],
|
||||
xmax = pos[2] + width,
|
||||
ymin = pos[1],
|
||||
ymax = pos[1] + height,
|
||||
}
|
||||
end
|
||||
|
||||
function M.overlap(win1, win2)
|
||||
local bb1 = M.bounds(win1)
|
||||
local bb2 = M.bounds(win2)
|
||||
|
||||
-- # determine the coordinates of the intersection rectangle
|
||||
local x_left = math.max(bb1["xmin"], bb2["xmin"])
|
||||
local y_top = math.max(bb1["ymin"], bb2["ymin"])
|
||||
local x_right = math.min(bb1["xmax"], bb2["xmax"])
|
||||
local y_bottom = math.min(bb1["ymax"], bb2["ymax"])
|
||||
|
||||
if x_right < x_left or y_bottom < y_top then
|
||||
return 0.0
|
||||
end
|
||||
|
||||
-- # The intersection of two axis-aligned bounding boxes is always an
|
||||
-- # axis-aligned bounding box
|
||||
local intersection_area = math.max(x_right - x_left, 1) * math.max(y_bottom - y_top, 1)
|
||||
|
||||
-- # compute the area of both AABBs
|
||||
local bb1_area = (bb1["xmax"] - bb1["xmin"]) * (bb1["ymax"] - bb1["ymin"])
|
||||
local bb2_area = (bb2["xmax"] - bb2["xmin"]) * (bb2["ymax"] - bb2["ymin"])
|
||||
|
||||
-- # compute the intersection over union by taking the intersection
|
||||
-- # area and dividing it by the sum of prediction + ground-truth
|
||||
-- # areas - the intersection area
|
||||
return intersection_area / (bb1_area + bb2_area - intersection_area)
|
||||
end
|
||||
|
||||
---@param opts? NuiPopupOptions
|
||||
---@return _.NuiPopupOptions
|
||||
function M.normalize_popup_options(opts)
|
||||
opts = vim.deepcopy(opts or {})
|
||||
|
||||
-- relative, position, size
|
||||
_.normalize_layout_options(opts)
|
||||
|
||||
-- border
|
||||
local border = opts.border
|
||||
if type(border) == "string" then
|
||||
opts.border = { style = border }
|
||||
end
|
||||
|
||||
-- border padding
|
||||
if opts.border then
|
||||
opts.border.padding = M.normalize_padding(opts.border)
|
||||
end
|
||||
|
||||
-- fix border text
|
||||
if opts.border and (not opts.border.style or opts.border.style == "none" or opts.border.style == "shadow") then
|
||||
opts.border.text = nil
|
||||
end
|
||||
return opts
|
||||
end
|
||||
|
||||
---@param opts? NuiSplitOptions
|
||||
---@return _.NuiSplitOptions
|
||||
function M.normalize_split_options(opts)
|
||||
opts = vim.deepcopy(opts or {})
|
||||
|
||||
-- relative
|
||||
require("nui.split.utils").normalize_options(opts)
|
||||
|
||||
return opts
|
||||
end
|
||||
|
||||
---@param hl string|table<string,string>
|
||||
function M.get_win_highlight(hl)
|
||||
if type(hl) == "string" then
|
||||
return hl
|
||||
end
|
||||
local ret = {}
|
||||
for key, value in pairs(hl) do
|
||||
table.insert(ret, key .. ":" .. value)
|
||||
end
|
||||
return table.concat(ret, ",")
|
||||
end
|
||||
|
||||
---@param opts? NuiBorder|_.NuiBorderStyle|_.NuiBorder
|
||||
---@return _.NuiBorderPadding
|
||||
function M.normalize_padding(opts)
|
||||
opts = opts or {}
|
||||
if type(opts) == "string" then
|
||||
opts = { style = opts }
|
||||
end
|
||||
|
||||
if Util.islist(opts.padding) then
|
||||
if #opts.padding == 2 then
|
||||
return {
|
||||
top = opts.padding[1],
|
||||
bottom = opts.padding[1],
|
||||
left = opts.padding[2],
|
||||
right = opts.padding[2],
|
||||
}
|
||||
elseif #opts.padding == 4 then
|
||||
return {
|
||||
top = opts.padding[1],
|
||||
right = opts.padding[2],
|
||||
bottom = opts.padding[3],
|
||||
left = opts.padding[4],
|
||||
}
|
||||
end
|
||||
end
|
||||
return vim.tbl_deep_extend("force", {
|
||||
left = 0,
|
||||
right = 0,
|
||||
top = 0,
|
||||
bottom = 0,
|
||||
}, opts.padding or {})
|
||||
end
|
||||
|
||||
function M.win_buf_height(win)
|
||||
local buf = vim.api.nvim_win_get_buf(win)
|
||||
|
||||
if not vim.wo[win].wrap then
|
||||
return vim.api.nvim_buf_line_count(buf)
|
||||
end
|
||||
|
||||
local width = vim.api.nvim_win_get_width(win)
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
|
||||
local height = 0
|
||||
for _, l in ipairs(lines) do
|
||||
height = height + math.max(1, (math.ceil(vim.fn.strwidth(l) / width)))
|
||||
end
|
||||
return height
|
||||
end
|
||||
|
||||
---@param dim {width: number, height:number}
|
||||
---@param _opts NoiceNuiOptions
|
||||
---@return _.NoiceNuiOptions
|
||||
function M.get_layout(dim, _opts)
|
||||
---@type _.NoiceNuiOptions
|
||||
local opts = M.normalize(_opts)
|
||||
|
||||
local position = vim.deepcopy(opts.position)
|
||||
local size = vim.deepcopy(opts.size)
|
||||
|
||||
---@return number
|
||||
local function minmax(min, max, value)
|
||||
return math.max(min or 1, math.min(value, max or 1000))
|
||||
end
|
||||
|
||||
if opts.type == "split" then
|
||||
---@cast opts _.NuiSplitOptions
|
||||
if size == "auto" then
|
||||
if position == "top" or position == "bottom" then
|
||||
size = minmax(opts.min_size, opts.max_size, dim.height)
|
||||
else
|
||||
size = minmax(opts.min_size, opts.max_size, dim.width)
|
||||
end
|
||||
end
|
||||
elseif opts.type == "popup" then
|
||||
if size.width == "auto" then
|
||||
size.width = minmax(size.min_width, size.max_width, dim.width)
|
||||
dim.width = size.width
|
||||
end
|
||||
if size.height == "auto" then
|
||||
size.height = minmax(size.min_height, size.max_height, dim.height)
|
||||
dim.height = size.height
|
||||
end
|
||||
if position and not (opts.relative and opts.relative.type == "cursor") then
|
||||
if type(position.col) == "number" and position.col < 0 then
|
||||
position.col = vim.o.columns + position.col - dim.width
|
||||
end
|
||||
if type(position.row) == "number" and position.row < 0 then
|
||||
position.row = vim.o.lines + position.row - dim.height
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return { size = size, position = position, relative = opts.relative }
|
||||
end
|
||||
|
||||
function M.anchor(width, height)
|
||||
local anchor = ""
|
||||
local lines_above = vim.fn.screenrow() - 1
|
||||
local lines_below = vim.fn.winheight(0) - lines_above
|
||||
|
||||
if height < lines_below then
|
||||
anchor = anchor .. "N"
|
||||
else
|
||||
anchor = anchor .. "S"
|
||||
end
|
||||
|
||||
if vim.go.columns - vim.fn.screencol() > width then
|
||||
anchor = anchor .. "W"
|
||||
else
|
||||
anchor = anchor .. "E"
|
||||
end
|
||||
return anchor
|
||||
end
|
||||
|
||||
function M.scroll(win, delta)
|
||||
local info = vim.fn.getwininfo(win)[1] or {}
|
||||
local top = info.topline or 1
|
||||
local buf = vim.api.nvim_win_get_buf(win)
|
||||
top = top + delta
|
||||
top = math.max(top, 1)
|
||||
top = math.min(top, M.win_buf_height(win) - info.height + 1)
|
||||
|
||||
vim.defer_fn(function()
|
||||
vim.api.nvim_buf_call(buf, function()
|
||||
vim.api.nvim_command("noautocmd silent! normal! " .. top .. "zt")
|
||||
vim.api.nvim_exec_autocmds("WinScrolled", { modeline = false })
|
||||
end)
|
||||
end, 0)
|
||||
end
|
||||
|
||||
return M
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,38 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Message = require("noice.message")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class NoiceStat
|
||||
---@field event string
|
||||
---@field count number
|
||||
|
||||
---@type table<string, NoiceStat>
|
||||
M._stats = {}
|
||||
|
||||
function M.reset()
|
||||
M._stats = {}
|
||||
end
|
||||
|
||||
function M.track(event)
|
||||
if not M._stats[event] then
|
||||
M._stats[event] = {
|
||||
event = event,
|
||||
count = 0,
|
||||
}
|
||||
end
|
||||
M._stats[event].count = M._stats[event].count + 1
|
||||
end
|
||||
|
||||
---@type NoiceMessage
|
||||
M._message = nil
|
||||
function M.message()
|
||||
if not M._message then
|
||||
M._message = Message("noice", "stats")
|
||||
end
|
||||
M._message:set(vim.inspect(M._stats))
|
||||
return M._message
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,101 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local View = require("noice.view")
|
||||
local NuiView = require("noice.view.nui")
|
||||
local Util = require("noice.util")
|
||||
|
||||
---@class NoiceMiniOptions
|
||||
---@field timeout integer
|
||||
---@field reverse? boolean
|
||||
local defaults = { timeout = 5000 }
|
||||
|
||||
---@class MiniView: NoiceView
|
||||
---@field active table<number, NoiceMessage>
|
||||
---@field super NoiceView
|
||||
---@field view? NuiView
|
||||
---@field timers table<number, vim.loop.Timer>
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local MiniView = View:extend("MiniView")
|
||||
|
||||
function MiniView:init(opts)
|
||||
MiniView.super.init(self, opts)
|
||||
self.active = {}
|
||||
self.timers = {}
|
||||
self._instance = "view"
|
||||
local view_opts = vim.deepcopy(self._opts)
|
||||
view_opts.type = "popup"
|
||||
view_opts.format = { "{message}" }
|
||||
view_opts.timeout = nil
|
||||
self.view = NuiView(view_opts)
|
||||
end
|
||||
|
||||
function MiniView:update_options()
|
||||
self._opts = vim.tbl_deep_extend("force", defaults, self._opts)
|
||||
end
|
||||
|
||||
---@param message NoiceMessage
|
||||
function MiniView:can_hide(message)
|
||||
if message.opts.keep and message.opts.keep() then
|
||||
return false
|
||||
end
|
||||
return not Util.is_blocking()
|
||||
end
|
||||
|
||||
function MiniView:autohide(id)
|
||||
if not self.timers[id] then
|
||||
self.timers[id] = vim.loop.new_timer()
|
||||
end
|
||||
self.timers[id]:start(self._opts.timeout, 0, function()
|
||||
if not self.active[id] then
|
||||
return
|
||||
end
|
||||
if not self:can_hide(self.active[id]) then
|
||||
return self:autohide(id)
|
||||
end
|
||||
self.active[id] = nil
|
||||
self.timers[id] = nil
|
||||
vim.schedule(function()
|
||||
self:update()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
function MiniView:show()
|
||||
for _, message in ipairs(self._messages) do
|
||||
-- we already have debug info,
|
||||
-- so make sure we dont regen it in the child view
|
||||
message._debug = true
|
||||
self.active[message.id] = message
|
||||
self:autohide(message.id)
|
||||
end
|
||||
self:clear()
|
||||
self:update()
|
||||
end
|
||||
|
||||
function MiniView:dismiss()
|
||||
self:clear()
|
||||
self.active = {}
|
||||
self:update()
|
||||
end
|
||||
|
||||
function MiniView:update()
|
||||
local active = vim.tbl_values(self.active)
|
||||
table.sort(
|
||||
active,
|
||||
---@param a NoiceMessage
|
||||
---@param b NoiceMessage
|
||||
function(a, b)
|
||||
local ret = a.id < b.id
|
||||
if self._opts.reverse then
|
||||
return not ret
|
||||
end
|
||||
return ret
|
||||
end
|
||||
)
|
||||
self.view:set(active)
|
||||
self.view:display()
|
||||
end
|
||||
|
||||
function MiniView:hide() end
|
||||
|
||||
return MiniView
|
||||
@ -0,0 +1,210 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Util = require("noice.util")
|
||||
local View = require("noice.view")
|
||||
local Manager = require("noice.message.manager")
|
||||
local NuiText = require("nui.text")
|
||||
|
||||
---@class NoiceNotifyOptions
|
||||
---@field title string
|
||||
---@field level? string|number Message log level
|
||||
---@field merge boolean Merge messages into one Notification or create separate notifications
|
||||
---@field replace boolean Replace existing notification or create a new one
|
||||
---@field render? notify.RenderFun|string
|
||||
---@field timeout? integer
|
||||
local defaults = {
|
||||
title = "Notification",
|
||||
merge = false,
|
||||
level = nil, -- vim.log.levels.INFO,
|
||||
replace = false,
|
||||
}
|
||||
|
||||
---@class NotifyInstance
|
||||
---@field notify fun(msg:string?, level?:string|number, opts?:table): notify.Record}
|
||||
|
||||
---@alias notify.RenderFun fun(buf:buffer, notif: Notification, hl: NotifyBufHighlights, config: notify.Config)
|
||||
|
||||
---@class NotifyView: NoiceView
|
||||
---@field win? number
|
||||
---@field buf? number
|
||||
---@field notif notify.Record
|
||||
---@field super NoiceView
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local NotifyView = View:extend("NotifyView")
|
||||
|
||||
function NotifyView.dismiss()
|
||||
require("notify").dismiss({ pending = true, silent = true })
|
||||
end
|
||||
|
||||
function NotifyView:is_available()
|
||||
return pcall(_G.require, "notify") == true
|
||||
end
|
||||
|
||||
function NotifyView:update_options()
|
||||
self._opts = vim.tbl_deep_extend("force", defaults, self._opts)
|
||||
end
|
||||
|
||||
function NotifyView:plain()
|
||||
return function(bufnr, notif)
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, notif.message)
|
||||
end
|
||||
end
|
||||
|
||||
---@param config notify.Config
|
||||
---@param render? notify.RenderFun|string
|
||||
---@return notify.RenderFun
|
||||
function NotifyView:get_render(config, render)
|
||||
---@type string|notify.RenderFun
|
||||
local ret = render or config.render()
|
||||
if type(ret) == "string" then
|
||||
if ret == "plain" then
|
||||
ret = self:plain()
|
||||
else
|
||||
---@type notify.RenderFun
|
||||
ret = require("notify.render")[ret]
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
---@param messages NoiceMessage[]
|
||||
---@param render? notify.RenderFun|string
|
||||
---@param content? string
|
||||
function NotifyView:notify_render(messages, render, content)
|
||||
---@param config notify.Config
|
||||
return function(buf, notif, hl, config)
|
||||
-- run notify view
|
||||
self:get_render(config, render)(buf, notif, hl, config)
|
||||
|
||||
Util.tag(buf, "notify")
|
||||
|
||||
---@type string[]
|
||||
local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
|
||||
|
||||
local text = table.concat(lines, "\n")
|
||||
local idx = content and text:find(content, 1, true) or nil
|
||||
|
||||
if idx then
|
||||
-- we found the offset of the content as a string
|
||||
local before = text:sub(1, idx - 1)
|
||||
local offset = #vim.split(before, "\n")
|
||||
local offset_col = #before:match("[^\n]*$")
|
||||
|
||||
-- in case the content starts in the middle of the line,
|
||||
-- we need to add a fake prefix to the first line of the first message
|
||||
-- see #375
|
||||
if offset_col > 0 then
|
||||
messages = vim.deepcopy(messages)
|
||||
table.insert(messages[1]._lines[1]._texts, 1, NuiText(string.rep(" ", offset_col)))
|
||||
end
|
||||
|
||||
-- do our rendering
|
||||
self:render(buf, { offset = offset, highlight = true, messages = messages })
|
||||
-- in case we didn't find the offset, we won't highlight anything
|
||||
end
|
||||
|
||||
-- resize notification
|
||||
local win = vim.fn.bufwinid(buf)
|
||||
if win ~= -1 then
|
||||
---@type number
|
||||
local width = config.minimum_width()
|
||||
for _, line in pairs(lines) do
|
||||
width = math.max(width, vim.str_utfindex(line))
|
||||
end
|
||||
width = math.min(config.max_width() or 1000, width)
|
||||
local height = math.min(config.max_height() or 1000, #lines)
|
||||
Util.win_apply_config(win, { width = width, height = height })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@alias NotifyMsg {content:string, messages:NoiceMessage[], title?:string, level?:string, opts?: table}
|
||||
|
||||
---@param msg NotifyMsg
|
||||
function NotifyView:_notify(msg)
|
||||
local level = self._opts.level or msg.level
|
||||
|
||||
local opts = {
|
||||
title = msg.title or self._opts.title,
|
||||
animate = not Util.is_blocking(),
|
||||
timeout = self._opts.timeout,
|
||||
replace = self._opts.replace and self.notif,
|
||||
keep = function()
|
||||
return Util.is_blocking()
|
||||
end,
|
||||
on_open = function(win)
|
||||
self:set_win_options(win)
|
||||
if self._opts.merge then
|
||||
self.win = win
|
||||
end
|
||||
end,
|
||||
on_close = function()
|
||||
self.notif = nil
|
||||
for _, m in ipairs(msg.messages) do
|
||||
m.opts.notify_id = nil
|
||||
end
|
||||
self.win = nil
|
||||
end,
|
||||
render = Util.protect(self:notify_render(msg.messages, self._opts.render, msg.content)),
|
||||
}
|
||||
|
||||
if msg.opts then
|
||||
opts = vim.tbl_deep_extend("force", opts, msg.opts)
|
||||
if type(msg.opts.replace) == "table" then
|
||||
local m = Manager.get_by_id(msg.opts.replace.id)
|
||||
opts.replace = m and m.opts.notify_id or nil
|
||||
elseif type(msg.opts.replace) == "number" then
|
||||
local m = Manager.get_by_id(msg.opts.replace)
|
||||
opts.replace = m and m.opts.notify_id or nil
|
||||
end
|
||||
end
|
||||
|
||||
---@type string?
|
||||
local content = msg.content
|
||||
|
||||
if msg.opts and msg.opts.is_nil then
|
||||
content = nil
|
||||
end
|
||||
|
||||
local id = require("notify")(content, level, opts)
|
||||
self.notif = id
|
||||
for _, m in ipairs(msg.messages) do
|
||||
m.opts.notify_id = id
|
||||
end
|
||||
end
|
||||
|
||||
function NotifyView:show()
|
||||
---@type NotifyMsg[]
|
||||
local todo = {}
|
||||
|
||||
if self._opts.merge then
|
||||
table.insert(todo, {
|
||||
content = self:content(),
|
||||
messages = self._messages,
|
||||
})
|
||||
else
|
||||
for _, m in ipairs(self._messages) do
|
||||
table.insert(todo, {
|
||||
content = m:content(),
|
||||
messages = { m },
|
||||
title = m.opts.title,
|
||||
level = m.level,
|
||||
opts = m.opts,
|
||||
})
|
||||
end
|
||||
end
|
||||
self:clear()
|
||||
|
||||
for _, msg in ipairs(todo) do
|
||||
self:_notify(msg)
|
||||
end
|
||||
end
|
||||
|
||||
function NotifyView:hide()
|
||||
if self.win and vim.api.nvim_win_is_valid(self.win) then
|
||||
vim.api.nvim_win_close(self.win, true)
|
||||
self.win = nil
|
||||
end
|
||||
end
|
||||
|
||||
return NotifyView
|
||||
@ -0,0 +1,165 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Util = require("noice.util")
|
||||
local View = require("noice.view")
|
||||
|
||||
---@class NoiceNotifySendOptions
|
||||
---@field title string
|
||||
---@field level? string|number Message log level
|
||||
---@field merge boolean Merge messages into one Notification or create separate notifications
|
||||
---@field replace boolean Replace existing notification or create a new one
|
||||
local defaults = {
|
||||
title = "Notification",
|
||||
merge = false,
|
||||
level = nil, -- vim.log.levels.INFO,
|
||||
replace = false,
|
||||
}
|
||||
|
||||
---@class NotifySendArgs
|
||||
---@field title? string
|
||||
---@field body string
|
||||
---@field app_name? string
|
||||
---@field urgency? string
|
||||
---@field expire_time? integer
|
||||
---@field icon? string
|
||||
---@field category? string
|
||||
---@field hint? string
|
||||
---@field print_id? boolean
|
||||
---@field replace_id? string
|
||||
|
||||
---@class NotifySendView: NoiceView
|
||||
---@field win? number
|
||||
---@field buf? number
|
||||
---@field notif? string
|
||||
---@field super NoiceView
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local NotifySendView = View:extend("NotifySendView")
|
||||
|
||||
function NotifySendView:init(opts)
|
||||
NotifySendView.super.init(self, opts)
|
||||
end
|
||||
|
||||
function NotifySendView:is_available()
|
||||
return vim.fn.executable("notify-send") == 1
|
||||
end
|
||||
|
||||
function NotifySendView:update_options()
|
||||
self._opts = vim.tbl_deep_extend("force", defaults, self._opts)
|
||||
end
|
||||
|
||||
---@alias NotifySendMsg {content:string, messages:NoiceMessage[], title?:string, level?:NotifyLevel, opts?: table}
|
||||
|
||||
---@param level? NotifyLevel|number
|
||||
function NotifySendView:get_urgency(level)
|
||||
if level then
|
||||
local l = type(level) == "number" and level or vim.log.levels[level:lower()] or vim.log.levels.INFO
|
||||
if l <= 1 then
|
||||
return "low"
|
||||
end
|
||||
if l >= 4 then
|
||||
return "critical"
|
||||
end
|
||||
end
|
||||
return "normal"
|
||||
end
|
||||
|
||||
---@param msg NotifySendMsg
|
||||
function NotifySendView:_notify(msg)
|
||||
local level = self._opts.level or msg.level
|
||||
|
||||
---@type NotifySendArgs
|
||||
local opts = {
|
||||
app_name = "nvim",
|
||||
icon = "nvim",
|
||||
title = msg.title or self._opts.title,
|
||||
body = msg.content,
|
||||
replace_id = self._opts.replace and self.notif or nil,
|
||||
urgency = self:get_urgency(level),
|
||||
}
|
||||
|
||||
local args = { "--print-id" }
|
||||
for k, v in pairs(opts) do
|
||||
if not (k == "title" or k == "body") then
|
||||
table.insert(args, "--" .. k:gsub("_", "-"))
|
||||
table.insert(args, v)
|
||||
end
|
||||
end
|
||||
if opts.title then
|
||||
table.insert(args, vim.trim(opts.title))
|
||||
end
|
||||
if opts.body then
|
||||
table.insert(args, vim.trim(opts.body))
|
||||
end
|
||||
local stdout = vim.loop.new_pipe()
|
||||
local stderr = vim.loop.new_pipe()
|
||||
|
||||
local out = ""
|
||||
local err = ""
|
||||
|
||||
local proc
|
||||
proc = vim.loop.spawn(
|
||||
"notify-send",
|
||||
{
|
||||
stdio = { nil, stdout, stderr },
|
||||
args = args,
|
||||
},
|
||||
vim.schedule_wrap(function(code, _signal) -- on exit
|
||||
stdout:close()
|
||||
stderr:close()
|
||||
proc:close()
|
||||
|
||||
if code ~= 0 then
|
||||
return Util.error("notify-send failed: %s", err)
|
||||
else
|
||||
self.notif = vim.trim(out)
|
||||
end
|
||||
end)
|
||||
)
|
||||
|
||||
vim.loop.read_start(stdout, function(_, data)
|
||||
if data then
|
||||
out = out .. data
|
||||
end
|
||||
end)
|
||||
vim.loop.read_start(stderr, function(_, data)
|
||||
if data then
|
||||
err = err .. data
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function NotifySendView:show()
|
||||
---@type NotifySendMsg[]
|
||||
local todo = {}
|
||||
|
||||
if self._opts.merge then
|
||||
table.insert(todo, {
|
||||
content = self:content(),
|
||||
messages = self._messages,
|
||||
})
|
||||
else
|
||||
for _, m in ipairs(self._messages) do
|
||||
table.insert(todo, {
|
||||
content = m:content(),
|
||||
messages = { m },
|
||||
title = m.opts.title,
|
||||
level = m.level,
|
||||
opts = m.opts,
|
||||
})
|
||||
end
|
||||
end
|
||||
self:clear()
|
||||
|
||||
for _, msg in ipairs(todo) do
|
||||
self:_notify(msg)
|
||||
end
|
||||
end
|
||||
|
||||
function NotifySendView:hide()
|
||||
if self.win and vim.api.nvim_win_is_valid(self.win) then
|
||||
vim.api.nvim_win_close(self.win, true)
|
||||
self.win = nil
|
||||
end
|
||||
end
|
||||
|
||||
return NotifySendView
|
||||
@ -0,0 +1,5 @@
|
||||
---@param opts? NoiceViewOptions
|
||||
return function(opts)
|
||||
opts.type = "popup"
|
||||
return require("noice.view.nui")(opts)
|
||||
end
|
||||
@ -0,0 +1,5 @@
|
||||
---@param opts? NoiceViewOptions
|
||||
return function(opts)
|
||||
opts.type = "split"
|
||||
return require("noice.view.nui")(opts)
|
||||
end
|
||||
@ -0,0 +1,35 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Config = require("noice.config")
|
||||
local View = require("noice.view")
|
||||
|
||||
---@class VirtualText: NoiceView
|
||||
---@field extmark? number
|
||||
---@field buf? number
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local VirtualText = View:extend("VirtualTextView")
|
||||
|
||||
function VirtualText:show()
|
||||
self:hide()
|
||||
self.buf = vim.api.nvim_get_current_buf()
|
||||
|
||||
---@type number, number
|
||||
local line, col = unpack(vim.api.nvim_win_get_cursor(0))
|
||||
line = line - 1
|
||||
|
||||
if self._messages[1] then
|
||||
self.extmark = vim.api.nvim_buf_set_extmark(self.buf, Config.ns, line, col, {
|
||||
virt_text_pos = "eol",
|
||||
virt_text = { { vim.trim(self._messages[1]:content()), self._opts.hl_group or "DiagnosticVirtualTextInfo" } },
|
||||
hl_mode = "combine",
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function VirtualText:hide()
|
||||
if self.extmark and vim.api.nvim_buf_is_valid(self.buf) then
|
||||
vim.api.nvim_buf_del_extmark(self.buf, Config.ns, self.extmark)
|
||||
end
|
||||
end
|
||||
|
||||
return VirtualText
|
||||
@ -0,0 +1,286 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Config = require("noice.config")
|
||||
local ConfigViews = require("noice.config.views")
|
||||
local Util = require("noice.util")
|
||||
local Object = require("nui.object")
|
||||
local Format = require("noice.text.format")
|
||||
|
||||
---@class NoiceViewBaseOptions
|
||||
---@field buf_options? table<string,any>
|
||||
---@field backend string
|
||||
---@field fallback string Fallback view in case the backend could not be loaded
|
||||
---@field format? NoiceFormat|string
|
||||
---@field align? NoiceAlign
|
||||
---@field lang? string
|
||||
---@field view string
|
||||
|
||||
---@alias NoiceViewOptions NoiceViewBaseOptions|NoiceNuiOptions|NoiceNotifyOptions
|
||||
|
||||
---@class NoiceView
|
||||
---@field _tick number
|
||||
---@field _messages NoiceMessage[]
|
||||
---@field _id integer
|
||||
---@field _opts NoiceViewOptions
|
||||
---@field _view_opts NoiceViewOptions
|
||||
---@field _route_opts NoiceViewOptions
|
||||
---@field _visible boolean
|
||||
---@field _instance "opts" | "view" | "backend"
|
||||
---@field _errors integer
|
||||
---@overload fun(opts?: NoiceViewOptions): NoiceView
|
||||
local View = Object("NoiceView")
|
||||
|
||||
---@type {view:NoiceView, opts:NoiceViewOptions}[]
|
||||
View._views = {}
|
||||
|
||||
---@param view string
|
||||
---@param opts NoiceViewOptions
|
||||
function View.get_view(view, opts)
|
||||
local opts_orig = vim.deepcopy(opts)
|
||||
opts = vim.tbl_deep_extend("force", ConfigViews.get_options(view), opts or {}, { view = view })
|
||||
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
opts.backend = opts.backend or opts.render or view
|
||||
|
||||
-- check if we already loaded this backend
|
||||
for _, v in ipairs(View._views) do
|
||||
if v.opts.view == opts.view then
|
||||
if v.view._instance == "opts" and vim.deep_equal(opts, v.opts) then
|
||||
return v.view
|
||||
end
|
||||
if v.view._instance == "view" then
|
||||
return v.view
|
||||
end
|
||||
end
|
||||
if v.opts.backend == opts.backend then
|
||||
if v.view._instance == "backend" then
|
||||
return v.view
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@type NoiceView
|
||||
local mod = require("noice.view.backend." .. opts.backend)
|
||||
local init_opts = vim.deepcopy(opts)
|
||||
local ret = mod(opts)
|
||||
if not ret:is_available() and opts.fallback then
|
||||
return View.get_view(opts.fallback, opts_orig)
|
||||
end
|
||||
table.insert(View._views, { view = ret, opts = init_opts })
|
||||
return ret
|
||||
end
|
||||
|
||||
local _id = 0
|
||||
---@param opts? NoiceViewOptions
|
||||
function View:init(opts)
|
||||
_id = _id + 1
|
||||
self._id = _id
|
||||
self._tick = 0
|
||||
self._messages = {}
|
||||
self._opts = opts or {}
|
||||
self._visible = false
|
||||
self._view_opts = vim.deepcopy(self._opts)
|
||||
self._instance = "opts"
|
||||
self._errors = 0
|
||||
self:update_options()
|
||||
end
|
||||
|
||||
function View:is_available()
|
||||
return true
|
||||
end
|
||||
|
||||
function View:update_options() end
|
||||
|
||||
---@param messages NoiceMessage|NoiceMessage[]
|
||||
---@param opts? {format?: boolean}
|
||||
function View:push(messages, opts)
|
||||
opts = opts or {}
|
||||
|
||||
messages = Util.islist(messages) and messages or { messages }
|
||||
---@cast messages NoiceMessage[]
|
||||
|
||||
for _, message in ipairs(messages) do
|
||||
if opts.format ~= false then
|
||||
message = Format.format(message, self._opts.format)
|
||||
end
|
||||
table.insert(self._messages, message)
|
||||
end
|
||||
end
|
||||
|
||||
function View:clear()
|
||||
self._messages = {}
|
||||
self._route_opts = {}
|
||||
end
|
||||
|
||||
function View:dismiss()
|
||||
self:clear()
|
||||
end
|
||||
|
||||
function View:check_options()
|
||||
---@type NoiceViewOptions
|
||||
local old = vim.deepcopy(self._opts)
|
||||
self._opts = vim.tbl_deep_extend("force", vim.deepcopy(self._view_opts), self._route_opts or {})
|
||||
self:update_options()
|
||||
if not vim.deep_equal(old, self._opts) then
|
||||
self:reset(old, self._opts)
|
||||
end
|
||||
end
|
||||
|
||||
---@param messages NoiceMessage[]
|
||||
---@param opts? {format?: boolean}
|
||||
function View:set(messages, opts)
|
||||
opts = opts or {}
|
||||
self:clear()
|
||||
self:push(messages, opts)
|
||||
end
|
||||
|
||||
function View:debug(msg)
|
||||
if Config.options.debug then
|
||||
Util.debug(("[%s] %s"):format(self._opts.view, vim.inspect(msg)))
|
||||
Util.debug(debug.traceback())
|
||||
end
|
||||
end
|
||||
|
||||
-- Safely destroys any create windows and buffers.
|
||||
-- This is needed to properly re-create views in case of E565 errors
|
||||
function View:destroy() end
|
||||
|
||||
function View:display()
|
||||
if #self._messages > 0 then
|
||||
Format.align(self._messages, self._opts.align)
|
||||
self:check_options()
|
||||
|
||||
Util.protect(function()
|
||||
self._errors = self._errors + 1
|
||||
self:show()
|
||||
self._errors = 0
|
||||
end, {
|
||||
catch = function(err)
|
||||
self:debug(err)
|
||||
self:destroy()
|
||||
end,
|
||||
})()
|
||||
|
||||
self._visible = true
|
||||
else
|
||||
if self._visible then
|
||||
self:hide()
|
||||
end
|
||||
self._visible = false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
---@param old NoiceViewOptions
|
||||
---@param new NoiceViewOptions
|
||||
function View:reset(old, new) end
|
||||
|
||||
function View:show()
|
||||
Util.error("Missing implementation `View:show()` for %s", self)
|
||||
end
|
||||
|
||||
function View:hide()
|
||||
Util.error("Missing implementation `View:hide()` for %s", self)
|
||||
end
|
||||
|
||||
---@param messages? NoiceMessage[]
|
||||
function View:height(messages)
|
||||
local ret = 0
|
||||
for _, m in ipairs(messages or self._messages) do
|
||||
ret = ret + m:height()
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
---@param messages? NoiceMessage[]
|
||||
function View:width(messages)
|
||||
local ret = 0
|
||||
for _, m in ipairs(messages or self._messages) do
|
||||
ret = math.max(ret, m:width())
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
function View:content()
|
||||
return table.concat(
|
||||
vim.tbl_map(
|
||||
---@param m NoiceMessage
|
||||
function(m)
|
||||
return m:content()
|
||||
end,
|
||||
self._messages
|
||||
),
|
||||
"\n"
|
||||
)
|
||||
end
|
||||
|
||||
function View:set_win_options(win)
|
||||
if self._opts.win_options then
|
||||
Util.wo(win, self._opts.win_options)
|
||||
end
|
||||
-- reset cursor on show
|
||||
vim.api.nvim_win_set_cursor(win, { 1, 0 })
|
||||
if self._opts.type == "split" then
|
||||
vim.schedule(function()
|
||||
-- this is needed to make the nui split behave with vim.go.splitkeep
|
||||
if win and vim.api.nvim_win_is_valid(win) then
|
||||
vim.api.nvim_win_set_cursor(win, { 1, 0 })
|
||||
vim.api.nvim_win_call(win, function()
|
||||
vim.cmd([[noautocmd silent! normal! zt]])
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
---@param buf number buffer number
|
||||
---@param opts? {offset: number, highlight: boolean, messages?: NoiceMessage[]} line number (1-indexed), if `highlight`, then only highlight
|
||||
function View:render(buf, opts)
|
||||
if not Config.is_running() then
|
||||
return
|
||||
end
|
||||
opts = opts or {}
|
||||
local linenr = opts.offset or 1
|
||||
|
||||
if self._opts.buf_options then
|
||||
Util.ignore_events(function()
|
||||
require("nui.utils")._.set_buf_options(buf, self._opts.buf_options)
|
||||
end)
|
||||
end
|
||||
|
||||
if self._opts.lang and not vim.b[buf].ts_highlight then
|
||||
if not pcall(vim.treesitter.start, buf, self._opts.lang) then
|
||||
vim.bo[buf].syntax = self._opts.lang
|
||||
end
|
||||
end
|
||||
|
||||
vim.api.nvim_buf_clear_namespace(buf, Config.ns, linenr - 1, -1)
|
||||
vim.b[buf].messages = {}
|
||||
|
||||
---@type number?
|
||||
local win = vim.fn.bufwinid(buf)
|
||||
if win == -1 then
|
||||
win = nil
|
||||
end
|
||||
local cursor = win and vim.api.nvim_win_get_cursor(win)
|
||||
|
||||
if not opts.highlight then
|
||||
vim.api.nvim_buf_set_lines(buf, linenr - 1, -1, false, {})
|
||||
end
|
||||
|
||||
for _, m in ipairs(opts.messages or self._messages) do
|
||||
if opts.highlight then
|
||||
m:highlight(buf, Config.ns, linenr)
|
||||
else
|
||||
m:render(buf, Config.ns, linenr)
|
||||
end
|
||||
linenr = linenr + m:height()
|
||||
end
|
||||
|
||||
if cursor then
|
||||
-- restore cursor
|
||||
pcall(vim.api.nvim_win_set_cursor, win, cursor)
|
||||
end
|
||||
end
|
||||
|
||||
return View
|
||||
@ -0,0 +1,337 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local View = require("noice.view")
|
||||
local Util = require("noice.util")
|
||||
local Scrollbar = require("noice.view.scrollbar")
|
||||
local Config = require("noice.config")
|
||||
|
||||
---@class NuiView: NoiceView
|
||||
---@field _nui? NuiPopup|NuiSplit
|
||||
---@field _loading? boolean
|
||||
---@field super NoiceView
|
||||
---@field _hider fun()
|
||||
---@field _timeout_timer vim.loop.Timer
|
||||
---@field _scroll NoiceScrollbar
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local NuiView = View:extend("NuiView")
|
||||
|
||||
function NuiView:init(opts)
|
||||
NuiView.super.init(self, opts)
|
||||
self._timer = vim.loop.new_timer()
|
||||
end
|
||||
|
||||
function NuiView:autohide()
|
||||
if self._opts.timeout then
|
||||
self._timer:start(self._opts.timeout, 0, function()
|
||||
if self._visible then
|
||||
vim.schedule(function()
|
||||
self:hide()
|
||||
end)
|
||||
end
|
||||
self._timer:stop()
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function NuiView:update_options()
|
||||
self._opts = vim.tbl_deep_extend("force", {}, {
|
||||
buf_options = {
|
||||
buftype = "nofile",
|
||||
filetype = "noice",
|
||||
},
|
||||
win_options = {
|
||||
wrap = false,
|
||||
foldenable = false,
|
||||
scrolloff = 0,
|
||||
sidescrolloff = 0,
|
||||
},
|
||||
}, self._opts, self:get_layout())
|
||||
|
||||
self._opts = Util.nui.normalize(self._opts)
|
||||
if self._opts.anchor == "auto" then
|
||||
if self._opts.type == "popup" and self._opts.size then
|
||||
local width = self._opts.size.width
|
||||
local height = self._opts.size.height
|
||||
if type(width) == "number" and type(height) == "number" then
|
||||
local col = self._opts.position and self._opts.position.col
|
||||
local row = self._opts.position and self._opts.position.row
|
||||
self._opts.anchor = Util.nui.anchor(width, height)
|
||||
if self._opts.anchor:find("S") and row then
|
||||
self._opts.position.row = -row + 1
|
||||
end
|
||||
if self._opts.anchor:find("E") and col then
|
||||
self._opts.position.col = -col
|
||||
end
|
||||
end
|
||||
else
|
||||
self._opts.anchor = "NW"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Check if other floating windows are overlapping and move out of the way
|
||||
function NuiView:smart_move()
|
||||
if not Config.options.smart_move.enabled then
|
||||
return
|
||||
end
|
||||
if not (self._opts.type == "popup" and self._opts.relative and self._opts.relative.type == "editor") then
|
||||
return
|
||||
end
|
||||
if not (self._nui.winid and vim.api.nvim_win_is_valid(self._nui.winid)) then
|
||||
return
|
||||
end
|
||||
if not (self._nui.border.winid and vim.api.nvim_win_is_valid(self._nui.border.winid)) then
|
||||
return
|
||||
end
|
||||
|
||||
local nui_win = self._nui.border._.type == "complex" and self._nui.border.winid or self._nui.winid
|
||||
|
||||
local wins = vim.tbl_filter(function(win)
|
||||
local ft = vim.bo[vim.api.nvim_win_get_buf(win)].filetype
|
||||
return win ~= self._nui.winid
|
||||
and ft ~= "noice"
|
||||
and not vim.tbl_contains(Config.options.smart_move.excluded_filetypes, ft)
|
||||
and not (self._nui.border and self._nui.border.winid == win)
|
||||
and vim.api.nvim_win_is_valid(win)
|
||||
and vim.api.nvim_win_get_config(win).relative == "editor"
|
||||
and Util.nui.overlap(nui_win, win) > 0.3
|
||||
end, vim.api.nvim_list_wins())
|
||||
|
||||
if #wins > 0 then
|
||||
-- local info = vim.tbl_map(function(win)
|
||||
-- local buf = vim.api.nvim_win_get_buf(win)
|
||||
-- return {
|
||||
-- win = win,
|
||||
-- buftype = vim.bo[buf].buftype,
|
||||
-- ft = vim.bo[buf].filetype,
|
||||
-- syntax = vim.bo[buf].syntax,
|
||||
-- text = table.concat(vim.api.nvim_buf_get_lines(buf, 0, -1, false), "\n"),
|
||||
-- name = vim.api.nvim_buf_get_name(buf),
|
||||
-- -- config = vim.api.nvim_win_get_config(win),
|
||||
-- area = Util.nui.overlap(nui_win, win),
|
||||
-- }
|
||||
-- end, wins)
|
||||
-- dumpp(info)
|
||||
local layout = self:get_layout()
|
||||
layout.position.row = 2
|
||||
self._nui:update_layout(layout)
|
||||
end
|
||||
end
|
||||
|
||||
function NuiView:create()
|
||||
if self._loading then
|
||||
return
|
||||
end
|
||||
self._loading = true
|
||||
-- needed, since Nui mutates the options
|
||||
local opts = vim.deepcopy(self._opts)
|
||||
self._nui = self._opts.type == "split" and require("nui.split")(opts) or require("nui.popup")(opts)
|
||||
|
||||
self:mount()
|
||||
self:update_layout()
|
||||
if self._opts.scrollbar ~= false then
|
||||
self._scroll = Scrollbar({
|
||||
winnr = self._nui.winid,
|
||||
padding = Util.nui.normalize_padding(self._opts.border),
|
||||
})
|
||||
self._scroll:mount()
|
||||
end
|
||||
self._loading = false
|
||||
end
|
||||
|
||||
function NuiView:mount()
|
||||
self._nui:mount()
|
||||
if self._opts.close and self._opts.close.events then
|
||||
self._nui:on(self._opts.close.events, function()
|
||||
self:hide()
|
||||
end, { once = false })
|
||||
end
|
||||
|
||||
if self._opts.close and self._opts.close.keys then
|
||||
self._nui:map("n", self._opts.close.keys, function()
|
||||
self:hide()
|
||||
end, { remap = false, nowait = true })
|
||||
end
|
||||
end
|
||||
|
||||
---@param old NoiceNuiOptions
|
||||
---@param new NoiceNuiOptions
|
||||
function NuiView:reset(old, new)
|
||||
self._timer:stop()
|
||||
if self._nui then
|
||||
local layout = false
|
||||
local diff = vim.tbl_filter(function(key)
|
||||
if vim.tbl_contains({ "relative", "size", "position" }, key) then
|
||||
layout = true
|
||||
return false
|
||||
end
|
||||
if key == "timeout" then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end, Util.diff_keys(old, new))
|
||||
|
||||
if #diff > 0 then
|
||||
self._nui:unmount()
|
||||
self._nui = nil
|
||||
self._visible = false
|
||||
elseif layout then
|
||||
if not pcall(self.update_layout, self) then
|
||||
self._nui:unmount()
|
||||
self._nui = nil
|
||||
self._visible = false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Destroys any create windows and buffers with vim.schedule
|
||||
-- This is needed to properly re-create views in case of E565 errors
|
||||
function NuiView:destroy()
|
||||
local nui = self._nui
|
||||
local scroll = self._scroll
|
||||
vim.schedule(function()
|
||||
if nui then
|
||||
nui._.loading = false
|
||||
nui._.mounted = true
|
||||
nui:unmount()
|
||||
end
|
||||
if scroll then
|
||||
scroll:hide()
|
||||
end
|
||||
end)
|
||||
self._nui = nil
|
||||
self._scroll = nil
|
||||
self._loading = false
|
||||
end
|
||||
|
||||
function NuiView:hide()
|
||||
self._timer:stop()
|
||||
if self._nui then
|
||||
self._visible = false
|
||||
|
||||
Util.protect(function()
|
||||
if self._nui and not self._visible then
|
||||
self:clear()
|
||||
self._nui:unmount()
|
||||
if self._scroll then
|
||||
self._scroll:hide()
|
||||
end
|
||||
end
|
||||
end, {
|
||||
finally = function()
|
||||
if self._nui then
|
||||
self._nui._.loading = false
|
||||
end
|
||||
end,
|
||||
retry_on_E11 = true,
|
||||
retry_on_E565 = true,
|
||||
})()
|
||||
end
|
||||
end
|
||||
|
||||
function NuiView:get_layout()
|
||||
local layout = Util.nui.get_layout({ width = self:width(), height = self:height() }, self._opts)
|
||||
if self._opts.type == "popup" then
|
||||
---@cast layout _.NuiPopupOptions
|
||||
if
|
||||
layout.size
|
||||
and type(layout.size.width) == "number"
|
||||
and layout.size.width < self:width()
|
||||
and self._opts.win_options
|
||||
and self._opts.win_options.wrap
|
||||
then
|
||||
local height = 0
|
||||
for _, m in ipairs(self._messages) do
|
||||
for _, l in ipairs(m._lines) do
|
||||
height = height + math.max(1, (math.ceil(l:width() / layout.size.width)))
|
||||
end
|
||||
end
|
||||
layout = Util.nui.get_layout({ width = self:width(), height = height }, self._opts)
|
||||
end
|
||||
end
|
||||
return layout
|
||||
end
|
||||
|
||||
function NuiView:tag()
|
||||
Util.tag(self._nui.bufnr, "nui." .. self._opts.type)
|
||||
if self._nui.border and self._nui.border.bufnr then
|
||||
Util.tag(self._nui.border.bufnr, "nui." .. self._opts.type .. ".border")
|
||||
end
|
||||
end
|
||||
|
||||
function NuiView:fix_border()
|
||||
if
|
||||
self._nui
|
||||
and self._nui.border
|
||||
and self._nui.border.winid
|
||||
and vim.api.nvim_win_is_valid(self._nui.border.winid)
|
||||
then
|
||||
local winhl = vim.api.nvim_win_get_option(self._nui.border.winid, "winhighlight") or ""
|
||||
if not winhl:find("IncSearch") then
|
||||
local hl = vim.split(winhl, ",")
|
||||
hl[#hl + 1] = "Search:"
|
||||
hl[#hl + 1] = "IncSearch:"
|
||||
winhl = table.concat(hl, ",")
|
||||
vim.api.nvim_win_set_option(self._nui.border.winid, "winhighlight", winhl)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function NuiView:update_layout()
|
||||
self._nui:update_layout(self:get_layout())
|
||||
end
|
||||
|
||||
function NuiView:is_mounted()
|
||||
if self._nui and self._nui.bufnr and not vim.api.nvim_buf_is_valid(self._nui.bufnr) then
|
||||
self._nui.bufnr = nil
|
||||
end
|
||||
|
||||
if self._nui and self._nui.winid and not vim.api.nvim_win_is_valid(self._nui.winid) then
|
||||
self._nui.winid = nil
|
||||
end
|
||||
|
||||
if self._nui and self._nui._.mounted and not self._nui.bufnr then
|
||||
self._nui._.mounted = false
|
||||
end
|
||||
|
||||
return self._nui and self._nui._.mounted and self._nui.bufnr
|
||||
end
|
||||
|
||||
function NuiView:show()
|
||||
if self._loading then
|
||||
return
|
||||
end
|
||||
|
||||
if not self._nui then
|
||||
self:create()
|
||||
end
|
||||
|
||||
if not self:is_mounted() then
|
||||
self:mount()
|
||||
end
|
||||
|
||||
vim.bo[self._nui.bufnr].modifiable = true
|
||||
self:render(self._nui.bufnr)
|
||||
vim.bo[self._nui.bufnr].modifiable = false
|
||||
|
||||
self._nui:show()
|
||||
if not self._nui.winid then
|
||||
return
|
||||
end
|
||||
self:tag()
|
||||
if not self._visible then
|
||||
self:set_win_options(self._nui.winid)
|
||||
self:update_layout()
|
||||
self:smart_move()
|
||||
end
|
||||
|
||||
if self._scroll then
|
||||
self._scroll.winnr = self._nui.winid
|
||||
self._scroll:show()
|
||||
end
|
||||
self:fix_border()
|
||||
self:autohide()
|
||||
end
|
||||
|
||||
return NuiView
|
||||
@ -0,0 +1,163 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Object = require("nui.object")
|
||||
local Util = require("noice.util")
|
||||
|
||||
---@class NoiceScrollbar
|
||||
---@field winnr integer
|
||||
---@field ns_id integer
|
||||
---@field autocmd_id integer
|
||||
---@field bar {bufnr:integer, winnr:integer}?
|
||||
---@field thumb {bufnr:integer, winnr:integer}?
|
||||
---@field visible boolean
|
||||
---@field opts ScrollbarOptions
|
||||
---@overload fun(opts?:ScrollbarOptions):NoiceScrollbar
|
||||
local Scrollbar = Object("NuiScrollbar")
|
||||
|
||||
---@class ScrollbarOptions
|
||||
local defaults = {
|
||||
winnr = 0,
|
||||
autohide = true,
|
||||
hl_group = {
|
||||
bar = "NoiceScrollbar",
|
||||
thumb = "NoiceScrollbarThumb",
|
||||
},
|
||||
---@type _.NuiBorderPadding
|
||||
padding = {
|
||||
top = 0,
|
||||
right = 0,
|
||||
bottom = 0,
|
||||
left = 0,
|
||||
},
|
||||
}
|
||||
|
||||
---@param opts? ScrollbarOptions
|
||||
function Scrollbar:init(opts)
|
||||
self.opts = vim.tbl_deep_extend("force", defaults, opts or {})
|
||||
self.winnr = self.opts.winnr == 0 and vim.api.nvim_get_current_win() or self.opts.winnr
|
||||
self.visible = false
|
||||
end
|
||||
|
||||
function Scrollbar:mount()
|
||||
self.autocmd_id = vim.api.nvim_create_autocmd({ "WinScrolled", "CursorMoved" }, {
|
||||
callback = function()
|
||||
self:update()
|
||||
end,
|
||||
})
|
||||
self:update()
|
||||
end
|
||||
|
||||
function Scrollbar:unmount()
|
||||
if self.autocmd_id then
|
||||
vim.api.nvim_del_autocmd(self.autocmd_id)
|
||||
self.autocmd_id = nil
|
||||
end
|
||||
self:hide()
|
||||
end
|
||||
|
||||
function Scrollbar:show()
|
||||
if not self.visible then
|
||||
self.visible = true
|
||||
self.bar = self:_open_win({ normal = self.opts.hl_group.bar })
|
||||
self.thumb = self:_open_win({ normal = self.opts.hl_group.thumb })
|
||||
end
|
||||
self:update()
|
||||
end
|
||||
|
||||
function Scrollbar:hide()
|
||||
if self.visible then
|
||||
self.visible = false
|
||||
local bar = self.bar
|
||||
if bar then
|
||||
pcall(vim.api.nvim_buf_delete, bar.bufnr, { force = true })
|
||||
pcall(vim.api.nvim_win_close, bar.winnr, true)
|
||||
self.bar = nil
|
||||
end
|
||||
|
||||
local thumb = self.thumb
|
||||
if thumb then
|
||||
pcall(vim.api.nvim_buf_delete, thumb.bufnr, { force = true })
|
||||
pcall(vim.api.nvim_win_close, thumb.winnr, true)
|
||||
self.thumb = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Scrollbar:update()
|
||||
if not vim.api.nvim_win_is_valid(self.winnr) then
|
||||
return self:hide()
|
||||
end
|
||||
|
||||
local pos = vim.api.nvim_win_get_position(self.winnr)
|
||||
|
||||
local dim = {
|
||||
row = pos[1] - self.opts.padding.top,
|
||||
col = pos[2] - self.opts.padding.left,
|
||||
width = vim.api.nvim_win_get_width(self.winnr) + self.opts.padding.left + self.opts.padding.right,
|
||||
height = vim.api.nvim_win_get_height(self.winnr) + self.opts.padding.top + self.opts.padding.bottom,
|
||||
}
|
||||
|
||||
local buf_height = Util.nui.win_buf_height(self.winnr)
|
||||
|
||||
if self.opts.autohide and dim.height >= buf_height then
|
||||
self:hide()
|
||||
return
|
||||
elseif not self.visible then
|
||||
self:show()
|
||||
end
|
||||
|
||||
if not (vim.api.nvim_win_is_valid(self.bar.winnr) and vim.api.nvim_win_is_valid(self.thumb.winnr)) then
|
||||
self:hide()
|
||||
self:show()
|
||||
end
|
||||
|
||||
local zindex = vim.api.nvim_win_get_config(self.winnr).zindex or 50
|
||||
|
||||
Util.win_apply_config(self.bar.winnr, {
|
||||
height = dim.height,
|
||||
width = 1,
|
||||
col = dim.col + dim.width - 1,
|
||||
row = dim.row,
|
||||
zindex = zindex + 1,
|
||||
})
|
||||
|
||||
local thumb_height = math.floor(dim.height * dim.height / buf_height + 0.5)
|
||||
thumb_height = math.max(1, thumb_height)
|
||||
|
||||
local pct = vim.api.nvim_win_get_cursor(self.winnr)[1] / buf_height
|
||||
|
||||
local thumb_offset = math.floor(pct * (dim.height - thumb_height) + 0.5)
|
||||
|
||||
Util.win_apply_config(self.thumb.winnr, {
|
||||
width = 1,
|
||||
height = thumb_height,
|
||||
row = dim.row + thumb_offset,
|
||||
col = dim.col + dim.width - 1, -- info.col was already added scrollbar offset.
|
||||
zindex = zindex + 2,
|
||||
})
|
||||
end
|
||||
|
||||
function Scrollbar:_open_win(opts)
|
||||
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||
Util.tag(bufnr, "scrollbar")
|
||||
local ret = {
|
||||
bufnr = bufnr,
|
||||
winnr = vim.api.nvim_open_win(bufnr, false, {
|
||||
relative = "editor",
|
||||
focusable = false,
|
||||
width = 1,
|
||||
-- HACK: height should be >=2 in case of winbar, which is inherited from the parent window
|
||||
-- Change back to 1 when the upstream issue is fixed.
|
||||
-- See https://github.com/neovim/neovim/issues/19464
|
||||
height = 2,
|
||||
row = 0,
|
||||
col = 0,
|
||||
style = "minimal",
|
||||
noautocmd = true,
|
||||
}),
|
||||
}
|
||||
vim.api.nvim_win_set_option(ret.winnr, "winhighlight", "Normal:" .. opts.normal)
|
||||
return ret
|
||||
end
|
||||
|
||||
return Scrollbar
|
||||
@ -0,0 +1,78 @@
|
||||
local require = require("noice.util.lazy")
|
||||
|
||||
local Config = require("noice.config")
|
||||
local Manager = require("noice.message.manager")
|
||||
local Format = require("noice.text.format")
|
||||
local pickers = require("telescope.pickers")
|
||||
local finders = require("telescope.finders")
|
||||
local conf = require("telescope.config").values
|
||||
local previewers = require("telescope.previewers")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@param message NoiceMessage
|
||||
function M.display(message)
|
||||
message = Format.format(message, "telescope")
|
||||
local line = message._lines[1]
|
||||
local hl = {}
|
||||
local byte = 0
|
||||
for _, text in ipairs(line._texts) do
|
||||
local hl_group = text.extmark and text.extmark.hl_group
|
||||
if hl_group then
|
||||
table.insert(hl, { { byte, byte + text:length() }, hl_group })
|
||||
end
|
||||
byte = byte + text:length()
|
||||
end
|
||||
return line:content(), hl
|
||||
end
|
||||
|
||||
function M.finder()
|
||||
local messages = Manager.get(Config.options.commands.history.filter, {
|
||||
history = true,
|
||||
sort = true,
|
||||
reverse = true,
|
||||
})
|
||||
return finders.new_table({
|
||||
results = messages,
|
||||
entry_maker = function(message)
|
||||
return {
|
||||
message = message,
|
||||
display = function(entry)
|
||||
return M.display(entry.message)
|
||||
end,
|
||||
ordinal = Format.format(message, "telescope"):content(),
|
||||
}
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
function M.previewer()
|
||||
return previewers.new_buffer_previewer({
|
||||
title = "Message",
|
||||
define_preview = function(self, entry, _status)
|
||||
vim.api.nvim_win_set_option(self.state.winid, "wrap", true)
|
||||
|
||||
---@type NoiceMessage
|
||||
local message = Format.format(entry.message, "telescope_preview")
|
||||
message:render(self.state.bufnr, Config.ns)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
function M.telescope(opts)
|
||||
pickers
|
||||
.new(opts, {
|
||||
results_title = "Noice",
|
||||
prompt_title = "Filter Noice",
|
||||
finder = M.finder(),
|
||||
sorter = conf.generic_sorter(opts),
|
||||
previewer = M.previewer(),
|
||||
})
|
||||
:find()
|
||||
end
|
||||
|
||||
return require("telescope").register_extension({
|
||||
exports = {
|
||||
noice = M.telescope,
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user