1

Regenerate nvim config

This commit is contained in:
2024-06-02 03:29:20 +02:00
parent 75eea0c030
commit ef2e28883d
5576 changed files with 604886 additions and 503 deletions

View File

@ -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

View File

@ -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,
})

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"}

View File

@ -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

View File

@ -0,0 +1,5 @@
local M = {}
function M.on_destroy() end
return M

View File

@ -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

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,
})

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
---@param opts? NoiceViewOptions
return function(opts)
opts.type = "popup"
return require("noice.view.nui")(opts)
end

View File

@ -0,0 +1,5 @@
---@param opts? NoiceViewOptions
return function(opts)
opts.type = "split"
return require("noice.view.nui")(opts)
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,
},
})