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 @@
github: rcarriga

View File

@ -0,0 +1,62 @@
# Taken from telescope
name: Generate docs
on:
push:
branches-ignore:
- master
jobs:
build-sources:
name: Generate docs
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-20.04
url: https://github.com/neovim/neovim/releases/download/v0.5.1/nvim-linux64.tar.gz
steps:
- uses: actions/checkout@v2
- run: date +%F > todays-date
- name: Restore cache for today's nightly.
uses: actions/cache@v2
with:
path: _neovim
key: ${{ runner.os }}-${{ matrix.url }}-${{ hashFiles('todays-date') }}
- name: Prepare
run: |
test -d _neovim || {
mkdir -p _neovim
curl -sL ${{ matrix.url }} | tar xzf - --strip-components=1 -C "${PWD}/_neovim"
}
mkdir -p ~/.local/share/nvim/site/pack/vendor/start
git clone --depth 1 https://github.com/nvim-lua/plenary.nvim ~/.local/share/nvim/site/pack/vendor/start/plenary.nvim
git clone --depth 1 https://github.com/tjdevries/tree-sitter-lua ~/.local/share/nvim/site/pack/vendor/start/tree-sitter-lua
ln -s $(pwd) ~/.local/share/nvim/site/pack/vendor/start
- name: Build parser
run: |
# We have to build the parser every single time to keep up with parser changes
cd ~/.local/share/nvim/site/pack/vendor/start/tree-sitter-lua
make dist
cd -
- name: Generating docs
run: |
export PATH="${PWD}/_neovim/bin:${PATH}"
export VIM="${PWD}/_neovim/share/nvim/runtime"
nvim --version
./scripts/docgen
- name: Update documentation
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COMMIT_MSG: |
docs: update doc/nvim-notify.txt
skip-checks: true
run: |
git config user.email "actions@github"
git config user.name "Github Actions"
git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git
git add doc/
# Only commit and push if we have changes
git diff --quiet && git diff --staged --quiet || (git commit -m "${COMMIT_MSG}"; git push origin HEAD:${GITHUB_REF})

View File

@ -0,0 +1,70 @@
name: nvim-notify Workflow
on:
- push
jobs:
style:
name: style
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: JohnnyMorganz/stylua-action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
version: latest
args: --check lua/ tests/
tests:
name: tests
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-20.04
url: https://github.com/neovim/neovim/releases/download/v0.5.1/nvim-linux64.tar.gz
steps:
- uses: actions/checkout@v2
- run: date +%F > todays-date
- name: Restore cache for today's nightly.
uses: actions/cache@v2
with:
path: _neovim
key: ${{ runner.os }}-x64-${{ hashFiles('todays-date') }}
- name: Prepare dependencies
run: |
test -d _neovim || {
mkdir -p _neovim
curl -sL ${{ matrix.url }} | tar xzf - --strip-components=1 -C "${PWD}/_neovim"
}
mkdir -p ~/.local/share/nvim/site/pack/vendor/start
git clone --depth 1 https://github.com/nvim-lua/plenary.nvim ~/.local/share/nvim/site/pack/vendor/start/plenary.nvim
ln -s $(pwd) ~/.local/share/nvim/site/pack/vendor/start
- name: Run tests
run: |
export PATH="${PWD}/_neovim/bin:${PATH}"
export VIM="${PWD}/_neovim/share/nvim/runtime"
nvim --version
./scripts/test
release:
name: release
if: ${{ github.ref == 'refs/heads/master' }}
needs:
- style
- tests
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: 20
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npx semantic-release

View File

@ -0,0 +1,3 @@
neovim/
plenary.nvim/
doc/tags

View File

@ -0,0 +1,12 @@
{
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
[
"@semantic-release/github",
{
"successComment": false
}
]
]
}

View File

@ -0,0 +1,21 @@
ARG NEOVIM_RELEASE=${NEOVIM_RELEASE:-https://github.com/neovim/neovim/releases/download/nightly/nvim-linux64.tar.gz}
FROM ubuntu
ARG NEOVIM_RELEASE
RUN apt-get update
RUN apt-get -y install git curl tar gcc g++
RUN mkdir /neovim
RUN curl -sL ${NEOVIM_RELEASE} | tar xzf - --strip-components=1 -C "/neovim"
RUN git clone --depth 1 https://github.com/nvim-lua/plenary.nvim
RUN git clone --depth 1 https://github.com/tjdevries/tree-sitter-lua
WORKDIR tree-sitter-lua
RUN mkdir -p build parser; \
cc -o ./build/parser.so -I ./src src/parser.c src/scanner.c -shared -Os -lstdc++ -fPIC; \
ln -s ../build/parser.so parser/lua.so;
RUN mkdir /notify
WORKDIR /notify
ENTRYPOINT ["bash", "-c", "PATH=/neovim/bin:${PATH} VIM=/neovim/share/nvim/runtime nvim --headless -c 'set rtp+=. | set rtp+=../plenary.nvim/ | set rtp+=../tree-sitter-lua/ | runtime! plugin/plenary.vim | luafile ./scripts/gendocs.lua' -c 'qa'"]

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Rónán Carrigan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,323 @@
# nvim-notify
A fancy, configurable, notification manager for NeoVim
![notify](https://user-images.githubusercontent.com/24252670/130856848-e8289850-028f-4f49-82f1-5ea1b8912f5e.gif)
Credit to [sunjon](https://github.com/sunjon) for [the design](https://neovim.discourse.group/t/wip-animated-notifications-plugin/448) that inspired the appearance of this plugin.
* [Installation](#Installation)
* [Usage](#usage)
- [Viewing History](#viewing-history)
* [Configuration](#configuration)
- [Setup](#setup)
- [Highlights](#highlights)
- [Render Style](#render-style)
- [Animation Style](#animation-style)
+ [Opening the window](#opening-the-window)
+ [Changing the window](#changing-the-window)
## Installation
### Prerequisites
Make sure to use a font which supported glyphs (icons), font can be found [here](https://github.com/ryanoasis/nerd-fonts).
24-bit colour is required, which can be enabled by adding this to your init.lua
```lua
vim.opt.termguicolors = true
```
Then you can install nvim-notify with the package manager of your choice.
[**dein**](https://github.com/Shougo/dein.vim):
```vim
call dein#add("rcarriga/nvim-notify")
```
[**vim-plug**](https://github.com/junegunn/vim-plug):
```vim
Plug 'rcarriga/nvim-notify'
```
[**packer**](https://github.com/wbthomason/packer.nvim):
```lua
use 'rcarriga/nvim-notify'
```
## Usage
Simply call the module with a message!
```lua
require("notify")("My super important message")
```
Other plugins can use the notification windows by setting it as your default notify function
```lua
vim.notify = require("notify")
```
You can supply a level to change the border highlighting
```lua
vim.notify("This is an error message", "error")
```
Updating an existing notification is also possible!
![](https://user-images.githubusercontent.com/24252670/152641078-92f3da72-f49f-4705-aec8-86512693445f.gif)
Use treesitter highlighting inside notifications with opacity changing
![](https://user-images.githubusercontent.com/24252670/165042795-565878a3-9c6d-4c0b-ab0d-6858515835c5.gif)
There are a number of custom options that can be supplied in a table as the third argument.
See `:h NotifyOptions` for details.
Sample code for the first GIF above:
```lua
local plugin = "My Awesome Plugin"
vim.notify("This is an error message.\nSomething went wrong!", "error", {
title = plugin,
on_open = function()
vim.notify("Attempting recovery.", vim.log.levels.WARN, {
title = plugin,
})
local timer = vim.loop.new_timer()
timer:start(2000, 0, function()
vim.notify({ "Fixing problem.", "Please wait..." }, "info", {
title = plugin,
timeout = 3000,
on_close = function()
vim.notify("Problem solved", nil, { title = plugin })
vim.notify("Error code 0x0395AF", 1, { title = plugin })
end,
})
end)
end,
})
```
You can also use plenary's async library to avoid using callbacks:
```lua
local async = require("plenary.async")
local notify = require("notify").async
async.run(function()
notify("Let's wait for this to close").events.close()
notify("It closed!")
end)
```
Set a custom filetype to take advantage of treesitter highlighting:
```lua
vim.notify(text, "info", {
title = "My Awesome Plugin",
on_open = function(win)
local buf = vim.api.nvim_win_get_buf(win)
vim.api.nvim_buf_set_option(buf, "filetype", "markdown")
end,
})
```
Check out the wiki for more examples!
### Viewing History
If you have [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) installed then you can use the `notify` extension to search the history:
```vim
:Telescope notify
```
or in lua
```lua
require('telescope').extensions.notify.notify(<opts>)
```
**Note:** If you lazy load `telescope` you should manually call `require("telescope").load_extension("notify")` before using the above commands. If you don't lazy load `telescope` then `notify` does this for you.
<p align="center">
<img src="https://user-images.githubusercontent.com/24252670/136264308-2fcdfe57-a8f6-4b34-8ea1-e3a8349bc581.png" />
</p>
There is a command to display a log of the history.
```vim
:Notifications
```
<p align="center">
<img src="https://user-images.githubusercontent.com/24252670/136264653-83038119-634b-48e7-8e8a-0edf4546efe2.png" />
</p>
You can get a list of past notifications with the history function
```lua
require("notify").history()
```
## Configuration
### Setup
You can optionally call the `setup` function to provide configuration options
See `:h notify.Config` for options and `:h notify.setup()` for default values.
### Highlights
You can define custom highlights by supplying highlight groups for each of the levels.
The naming scheme follows a simple structure: `Notify<upper case level name><section>`.
If you want to use custom levels, you can define the highlights for them or
they will follow the `INFO` highlights by default.
Here are the defaults:
```vim
highlight NotifyERRORBorder guifg=#8A1F1F
highlight NotifyWARNBorder guifg=#79491D
highlight NotifyINFOBorder guifg=#4F6752
highlight NotifyDEBUGBorder guifg=#8B8B8B
highlight NotifyTRACEBorder guifg=#4F3552
highlight NotifyERRORIcon guifg=#F70067
highlight NotifyWARNIcon guifg=#F79000
highlight NotifyINFOIcon guifg=#A9FF68
highlight NotifyDEBUGIcon guifg=#8B8B8B
highlight NotifyTRACEIcon guifg=#D484FF
highlight NotifyERRORTitle guifg=#F70067
highlight NotifyWARNTitle guifg=#F79000
highlight NotifyINFOTitle guifg=#A9FF68
highlight NotifyDEBUGTitle guifg=#8B8B8B
highlight NotifyTRACETitle guifg=#D484FF
highlight link NotifyERRORBody Normal
highlight link NotifyWARNBody Normal
highlight link NotifyINFOBody Normal
highlight link NotifyDEBUGBody Normal
highlight link NotifyTRACEBody Normal
```
### Render Style
The appearance of the notifications can be configured, using either built-in methods or custom functions.
See `:help notify-render()` for details
1. "default"
![default](https://user-images.githubusercontent.com/24252670/141534868-fdcc9d03-9f7b-47fd-acfc-5a20b98e4e0a.png)
2. "minimal"
![image](https://user-images.githubusercontent.com/24252670/141534952-bb0cf491-5bb4-473c-9a67-8adb5b23b232.png)
3. "simple"
![image](https://user-images.githubusercontent.com/24252670/191683325-220208a0-90bf-4daa-b375-01b573ca524c.png)
4. "compact"
![image](https://user-images.githubusercontent.com/24252670/212632432-86621888-f885-4074-aed4-d12b5e291ab2.png)
5. "wrapped-compact"
Mostly same as `compact`, but lines are wrapped based on `max_width`, some padding is added.
![image](https://github.com/rcarriga/nvim-notify/assets/73286100/72237d45-6e3b-4c2a-8010-513a26871682)
Feel free to submit custom rendering functions to share with others!
### Animation Style
The animation is designed to work in stages. The first stage is the opening of
the window, and all subsequent stages can changes the position or opacity of
the window. You can use one of the built-in styles or provide your own in the setup.
1. "fade_in_slide_out"
![fade_slide](https://user-images.githubusercontent.com/24252670/130924913-f3a61f2c-2330-4426-a787-3cd7494fccc0.gif)
2. "fade"
![fade](https://user-images.githubusercontent.com/24252670/130924911-a89bef9b-e815-4aa5-a255-84bc23dd8c8e.gif)
3. "slide"
![slide](https://user-images.githubusercontent.com/24252670/130924905-656cabfc-9eb7-4e22-b6da-8a2a1f508fa5.gif)
4. "static"
![static](https://user-images.githubusercontent.com/24252670/130924902-8c77b5a1-6d13-48f4-98a9-866e58cb76e4.gif)
Custom styles can be provided by setting the config `stages` value to a list of
functions.
If you create a custom style, feel free to open a PR to submit it as a built-in style!
**NB.** This is a prototype API that is open to change. I am looking for
feedback on both issues or extra data that could be useful in creating
animation styles.
Check the [built-in styles](./lua/notify/stages/) to see examples
#### Opening the window
The first function in the list should return a table to be provided to
`nvim_open_win`, optionally including an extra `opacity` key which can be
between 0-100.
The function is given a state table that contains the following keys:
- `message: table` State of the message to be shown
- `width` Width of the message buffer
- `height` Height of the message buffer
- `open_windows: integer[]` List of all window IDs currently showing messages
- `buffer: integer` The buffer containing the rendered notification message.
If a notification can't be shown at the moment the function should return `nil`.
#### Changing the window
All following functions should return the goal values for the window to reach from it's current point.
They will receive the same state object as the initial function and a second argument of the window ID.
The following fields can be returned in a table:
- `col`
- `row`
- `height`
- `width`
- `opacity`
These can be provided as either numbers or as a table. If they are
provided as numbers then they will change instantly the value given.
If they are provided as a table, they will be treated as a value to animate towards.
This uses a dampened spring algorithm to provide a natural feel to the movement.
The table must contain the goal value as the 1st index (e.g. `{10}`)
All other values are provided with keys:
- `damping: number` How motion decays over time. Values less than 1 mean the spring can overshoot.
- Bounds: >= 0
- Default: 1
- `frequency: number` How fast the spring oscillates
- Bounds: >= 0
- Default: 1
- `complete: fun(value: number): bool` Function to determine if value has reached its goal. If not
provided it will complete when the value rounded to 2 decimal places is equal
to the goal.
Once the last function has reached its goals, the window is removed.
One of the stages should also return the key `time` set to true. This is
treated as the stage which the notification is on a timer. The goals of this
stage are not used to check if it is complete. The next stage will start
once the notification reaches its timeout.

View File

@ -0,0 +1,235 @@
*nvim-notify.txt* A fancy, configurable notification manager for NeoVim
==============================================================================
A fancy, configurable notification manager for NeoVim
notify *notify*
*notify.setup()*
`setup`({user_config})
Configure nvim-notify
See: ~
|notify.Config|
|notify-render|
Parameters~
{user_config} `(notify.Config|nil)`
Default values:
>lua
{
background_colour = "NotifyBackground",
fps = 30,
icons = {
DEBUG = "",
ERROR = "",
INFO = "",
TRACE = "✎",
WARN = ""
},
level = 2,
minimum_width = 50,
render = "default",
stages = "fade_in_slide_out",
time_formats = {
notification = "%T",
notification_history = "%FT%T"
},
timeout = 5000,
top_down = true
}
<
*notify.Options*
Options for an individual notification
Fields~
{title} `(string)`
{icon} `(string)`
{timeout} `(number|boolean)` Time to show notification in milliseconds, set to false to disable timeout.
{on_open} `(function)` Callback for when window opens, receives window as argument.
{on_close} `(function)` Callback for when window closes, receives window as argument.
{keep} `(function)` Function to keep the notification window open after timeout, should return boolean.
{render} `(function|string)` Function to render a notification buffer.
{replace} `(integer|notify.Record)` Notification record or the record `id` field. Replace an existing notification if still open. All arguments not given are inherited from the replaced notification including message and level.
{hide_from_history} `(boolean)` Hide this notification from the history
{animate} `(boolean)` If false, the window will jump to the timed stage. Intended for use in blocking events (e.g. vim.fn.input)
*notify.Events*
Async events for a notification
Fields~
{open} `(function)` Resolves when notification is opened
{close} `(function)` Resolved when notification is closed
*notify.Record*
Record of a previously sent notification
Fields~
{id} `(integer)`
{message} `(string[])` Lines of the message
{level} `(string|integer)` Log level. See vim.log.levels
{title} `(string[])` Left and right sections of the title
{icon} `(string)` Icon used for notification
{time} `(number)` Time of message, as returned by `vim.fn.localtime()`
{render} `(function)` Function to render notification buffer
*notify.AsyncRecord*
Inherits: `notify.Record`
Fields~
{events} `(notify.Events)`
*notify.notify()*
`notify`({message}, {level}, {opts})
Display a notification.
You can call the module directly rather than using this:
>lua
require("notify")(message, level, opts)
<
Parameters~
{message} `(string|string[])` Notification message
{level} `(string|number)` Log level. See vim.log.levels
{opts} `(notify.Options)` Notification options
Return~
`(notify.Record)`
*notify.async()*
`async`({message}, {level}, {opts})
Display a notification asynchronously
This uses plenary's async library, allowing a cleaner interface for
open/close events. You must call this function within an async context.
The `on_close` and `on_open` options are not used.
Parameters~
{message} `(string|string[])` Notification message
{level} `(string|number)` Log level. See vim.log.levels
{opts} `(notify.Options)` Notification options
Return~
`(notify.AsyncRecord)`
*notify.history()*
`history`({opts})
Get records of all previous notifications
You can use the `:Notifications` command to display a log of previous notifications
Parameters~
{opts?} `(notify.HistoryOpts)`
Return~
`(notify.Record[])`
*notify.HistoryOpts*
Fields~
{include_hidden} `(boolean)` Include notifications hidden from history
*notify.dismiss()*
`dismiss`({opts})
Dismiss all notification windows currently displayed
Parameters~
{opts} `(notify.DismissOpts)`
*notify.DismissOpts*
Fields~
{pending} `(boolean)` Clear pending notifications
{silent} `(boolean)` Suppress notification that pending notifications were dismissed.
*notify.open()*
`open`({notif_id}, {opts})
Open a notification in a new buffer
Parameters~
{notif_id} `(integer|notify.Record)`
{opts} `(notify.OpenOpts)`
Return~
`(notify.OpenedBuffer)`
*notify.OpenOpts*
Fields~
{buffer} `(integer)` Use this buffer, instead of creating a new one
{max_width} `(integer)` Render message to this width (used to limit window decoration sizes)
*notify.OpenedBuffer*
Fields~
{buffer} `(integer)` Created buffer number
{height} `(integer)` Height of the buffer content including extmarks
{width} `(integer)` width of the buffer content including extmarks
{highlights} `(table<string, string>)` Highlights used for the buffer contents
*notify.pending()*
`pending`()
Number of notifications currently waiting to be displayed
Return~
`(integer[])`
*notify.instance()*
`instance`({user_config}, {inherit})
Configure an instance of nvim-notify.
You can use this to manage a separate instance of nvim-notify with completely different configuration.
The returned instance will have the same functions as the notify module.
Parameters~
{user_config} `(notify.Config)`
{inherit?} `(boolean)` Inherit the global configuration, default true
==============================================================================
notify.config *notify.config*
*notify.Config*
Fields~
{level} `(string|integer)` Minimum log level to display. See vim.log.levels.
{timeout} `(number)` Default timeout for notification
{max_width} `(number|function)` Max number of columns for messages
{max_height} `(number|function)` Max number of lines for a message
{stages} `(string|function[])` Animation stages
{background_colour} `(string)` For stages that change opacity this is treated as the highlight behind the window. Set this to either a highlight group, an RGB hex value e.g. "#000000" or a function returning an RGB code for dynamic values
{icons} `(table)` Icons for each level (upper case names)
{time_formats} `(table)` Time formats for different kind of notifications
{on_open} `(function)` Function called when a new window is opened, use for changing win settings/config
{on_close} `(function)` Function called when a window is closed
{render} `(function|string)` Function to render a notification buffer or a built-in renderer name
{minimum_width} `(integer)` Minimum width for notification windows
{fps} `(integer)` Frames per second for animation stages, higher value means smoother animations but more CPU usage
{top_down} `(boolean)` whether or not to position the notifications at the top or not
==============================================================================
notify-render *notify-render*
Notification buffer rendering
Custom rendering can be provided by both the user config in the setup or on
an individual notification using the `render` key.
The key can either be the name of a built-in renderer or a custom function.
Built-in renderers:
- `"default"`
- `"minimal"`
- `"simple"`
- `"compact"`
- `"wrapped-compact"`
Custom functions should accept a buffer, a notification record and a highlights table
>
render: fun(buf: integer, notification: notify.Record, highlights: notify.Highlights, config)
<
You should use the provided highlight groups to take advantage of opacity
changes as they will be updated as the notification is animated
*notify.Highlights*
Fields~
{title} `(string)`
{icon} `(string)`
{border} `(string)`
{body} `(string)`
vim:tw=78:ts=8:noet:ft=help:norl:

View File

@ -0,0 +1,5 @@
local M = {}
M.spring = require("notify.animate.spring")
return M

View File

@ -0,0 +1,56 @@
-- Adapted from https://gist.github.com/Fraktality/1033625223e13c01aa7144abe4aaf54d
-- Explanation found here https://www.ryanjuckett.com/damped-springs/
local pi = math.pi
local exp = math.exp
local sin = math.sin
local cos = math.cos
local sqrt = math.sqrt
---@class SpringState
---@field position number
---@field velocity number | nil
---@param dt number @Step in time
---@param state SpringState
return function(dt, goal, state, frequency, damping)
local angular_freq = frequency * 2 * pi
local cur_vel = state.velocity or 0
local offset = state.position - goal
local decay = exp(-dt * damping * angular_freq)
local new_pos
local new_vel
if damping == 1 then -- critically damped
new_pos = (cur_vel * dt + offset * (angular_freq * dt + 1)) * decay + goal
new_vel = (cur_vel - angular_freq * dt * (offset * angular_freq + cur_vel)) * decay
elseif damping < 1 then -- underdamped
local c = sqrt(1 - damping * damping)
local i = cos(angular_freq * c * dt)
local j = sin(angular_freq * c * dt)
new_pos = (i * offset + j * (cur_vel + damping * angular_freq * offset) / (angular_freq * c))
* decay
+ goal
new_vel = (i * c * cur_vel - j * (cur_vel * damping + angular_freq * offset)) * decay / c
else -- overdamped
local c = sqrt(damping * damping - 1)
local r1 = -angular_freq * (damping - c)
local r2 = -angular_freq * (damping + c)
local co2 = (cur_vel - r1 * offset) / (2 * angular_freq * c)
local co1 = offset - co2
local e1 = co1 * exp(r1 * dt)
local e2 = co2 * exp(r2 * dt)
new_pos = e1 + e2 + goal
new_pos = r1 * e1 + r2 * e2
end
state.position = new_pos
state.velocity = new_vel
end

View File

@ -0,0 +1,41 @@
local M = {}
function M.setup()
vim.cmd([[
hi default link NotifyBackground Normal
hi default NotifyERRORBorder guifg=#8A1F1F
hi default NotifyWARNBorder guifg=#79491D
hi default NotifyINFOBorder guifg=#4F6752
hi default NotifyDEBUGBorder guifg=#8B8B8B
hi default NotifyTRACEBorder guifg=#4F3552
hi default NotifyERRORIcon guifg=#F70067
hi default NotifyWARNIcon guifg=#F79000
hi default NotifyINFOIcon guifg=#A9FF68
hi default NotifyDEBUGIcon guifg=#8B8B8B
hi default NotifyTRACEIcon guifg=#D484FF
hi default NotifyERRORTitle guifg=#F70067
hi default NotifyWARNTitle guifg=#F79000
hi default NotifyINFOTitle guifg=#A9FF68
hi default NotifyDEBUGTitle guifg=#8B8B8B
hi default NotifyTRACETitle guifg=#D484FF
hi default link NotifyERRORBody Normal
hi default link NotifyWARNBody Normal
hi default link NotifyINFOBody Normal
hi default link NotifyDEBUGBody Normal
hi default link NotifyTRACEBody Normal
hi default link NotifyLogTime Comment
hi default link NotifyLogTitle Special
]])
end
M.setup()
vim.cmd([[
augroup NvimNotifyRefreshHighlights
autocmd!
autocmd ColorScheme * lua require('notify.config.highlights').setup()
augroup END
]])
return M

View File

@ -0,0 +1,202 @@
---@tag notify.config
local Config = {}
local util = require("notify.util")
require("notify.config.highlights")
local BUILTIN_RENDERERS = {
DEFAULT = "default",
MINIMAL = "minimal",
}
local BUILTIN_STAGES = {
FADE = "fade",
SLIDE = "slide",
FADE_IN_SLIDE_OUT = "fade_in_slide_out",
STATIC = "static",
}
local default_config = {
level = vim.log.levels.INFO,
timeout = 5000,
max_width = nil,
max_height = nil,
stages = BUILTIN_STAGES.FADE_IN_SLIDE_OUT,
render = BUILTIN_RENDERERS.DEFAULT,
background_colour = "NotifyBackground",
on_open = nil,
on_close = nil,
minimum_width = 50,
fps = 30,
top_down = true,
time_formats = {
notification_history = "%FT%T",
notification = "%T",
},
icons = {
ERROR = "",
WARN = "",
INFO = "",
DEBUG = "",
TRACE = "",
},
}
---@class notify.Config
---@field level string|integer Minimum log level to display. See vim.log.levels.
---@field timeout number Default timeout for notification
---@field max_width number|function Max number of columns for messages
---@field max_height number|function Max number of lines for a message
---@field stages string|function[] Animation stages
---@field background_colour string For stages that change opacity this is treated as the highlight behind the window. Set this to either a highlight group, an RGB hex value e.g. "#000000" or a function returning an RGB code for dynamic values
---@field icons table Icons for each level (upper case names)
---@field time_formats table Time formats for different kind of notifications
---@field on_open function Function called when a new window is opened, use for changing win settings/config
---@field on_close function Function called when a window is closed
---@field render function|string Function to render a notification buffer or a built-in renderer name
---@field minimum_width integer Minimum width for notification windows
---@field fps integer Frames per second for animation stages, higher value means smoother animations but more CPU usage
---@field top_down boolean whether or not to position the notifications at the top or not
local opacity_warned = false
local function validate_highlight(colour_or_group, needs_opacity)
if type(colour_or_group) == "function" then
return colour_or_group
end
if colour_or_group:sub(1, 1) == "#" then
return function()
return colour_or_group
end
end
return function()
local group = vim.api.nvim_get_hl_by_name(colour_or_group, true)
if not group or not group.background then
if needs_opacity and not opacity_warned then
opacity_warned = true
vim.schedule(function()
vim.notify("Highlight group '" .. colour_or_group .. [[' has no background highlight
Please provide an RGB hex value or highlight group with a background value for 'background_colour' option.
This is the colour that will be used for 100% transparency.
```lua
require("notify").setup({
background_colour = "#000000",
})
```
Defaulting to #000000]], "warn", {
title = "nvim-notify",
on_open = function(win)
local buf = vim.api.nvim_win_get_buf(win)
vim.api.nvim_buf_set_option(buf, "filetype", "markdown")
end,
})
end)
end
return "#000000"
end
return string.format("#%x", group.background)
end
end
function Config._format_default()
local lines = { "Default values:", ">lua" }
for line in vim.gsplit(vim.inspect(default_config), "\n", true) do
table.insert(lines, " " .. line)
end
table.insert(lines, "<")
return lines
end
function Config.setup(custom_config)
local user_config = vim.tbl_deep_extend("keep", custom_config or {}, default_config)
local config = {}
function config.merged()
return user_config
end
function config.level()
local level = user_config.level
if type(level) == "number" then
return level
end
return vim.log.levels[vim.fn.toupper(level)] or vim.log.levels.INFO
end
function config.fps()
return user_config.fps
end
function config.background_colour()
return tonumber(user_config.background_colour():gsub("#", "0x"), 16)
end
function config.time_formats()
return user_config.time_formats
end
function config.icons()
return user_config.icons
end
function config.stages()
return user_config.stages
end
function config.default_timeout()
return user_config.timeout
end
function config.on_open()
return user_config.on_open
end
function config.top_down()
return user_config.top_down
end
function config.on_close()
return user_config.on_close
end
function config.render()
return user_config.render
end
function config.minimum_width()
return user_config.minimum_width
end
function config.max_width()
return util.is_callable(user_config.max_width) and user_config.max_width()
or user_config.max_width
end
function config.max_height()
return util.is_callable(user_config.max_height) and user_config.max_height()
or user_config.max_height
end
local stages = config.stages()
local needs_opacity =
vim.tbl_contains({ BUILTIN_STAGES.FADE_IN_SLIDE_OUT, BUILTIN_STAGES.FADE }, stages)
if needs_opacity and not vim.opt.termguicolors:get() and vim.fn.has("nvim-0.10") == 0 then
user_config.stages = BUILTIN_STAGES.STATIC
vim.schedule(function()
vim.notify(
"Opacity changes require termguicolors to be set.\nChange to different animation stages or set termguicolors to disable this warning",
"warn",
{ title = "nvim-notify" }
)
end)
end
user_config.background_colour = validate_highlight(user_config.background_colour, needs_opacity)
return config
end
return Config

View File

@ -0,0 +1,199 @@
---@text
--- A fancy, configurable notification manager for NeoVim
local config = require("notify.config")
local instance = require("notify.instance")
---@class notify
local notify = {}
local global_instance, global_config
--- Configure nvim-notify
--- See: ~
--- |notify.Config|
--- |notify-render|
---
---@param user_config notify.Config|nil
---@eval return require('notify.config')._format_default()
function notify.setup(user_config)
global_instance, global_config = notify.instance(user_config)
local has_telescope = (vim.fn.exists("g:loaded_telescope") == 1)
if has_telescope then
require("telescope").load_extension("notify")
end
vim.cmd([[command! Notifications :lua require("notify")._print_history()<CR>]])
end
function notify._config()
return config.setup(global_config)
end
---@class notify.Options
--- Options for an individual notification
---@field title string
---@field icon string
---@field timeout number|boolean Time to show notification in milliseconds, set to false to disable timeout.
---@field on_open function Callback for when window opens, receives window as argument.
---@field on_close function Callback for when window closes, receives window as argument.
---@field keep function Function to keep the notification window open after timeout, should return boolean.
---@field render function|string Function to render a notification buffer.
---@field replace integer|notify.Record Notification record or the record `id` field. Replace an existing notification if still open. All arguments not given are inherited from the replaced notification including message and level.
---@field hide_from_history boolean Hide this notification from the history
---@field animate boolean If false, the window will jump to the timed stage. Intended for use in blocking events (e.g. vim.fn.input)
---@class notify.Events
--- Async events for a notification
---@field open function Resolves when notification is opened
---@field close function Resolved when notification is closed
---@class notify.Record
--- Record of a previously sent notification
---@field id integer
---@field message string[] Lines of the message
---@field level string|integer Log level. See vim.log.levels
---@field title string[] Left and right sections of the title
---@field icon string Icon used for notification
---@field time number Time of message, as returned by `vim.fn.localtime()`
---@field render function Function to render notification buffer
---@class notify.AsyncRecord : notify.Record
---@field events notify.Events
--- Display a notification.
---
--- You can call the module directly rather than using this:
--- >lua
--- require("notify")(message, level, opts)
--- <
---@param message string|string[] Notification message
---@param level string|number Log level. See vim.log.levels
---@param opts notify.Options Notification options
---@return notify.Record
function notify.notify(message, level, opts)
if not global_instance then
notify.setup()
end
return global_instance.notify(message, level, opts)
end
--- Display a notification asynchronously
---
--- This uses plenary's async library, allowing a cleaner interface for
--- open/close events. You must call this function within an async context.
---
--- The `on_close` and `on_open` options are not used.
---
---@param message string|string[] Notification message
---@param level string|number Log level. See vim.log.levels
---@param opts notify.Options Notification options
---@return notify.AsyncRecord
function notify.async(message, level, opts)
if not global_instance then
notify.setup()
end
return global_instance.async(message, level, opts)
end
--- Get records of all previous notifications
---
--- You can use the `:Notifications` command to display a log of previous notifications
---@param opts? notify.HistoryOpts
---@return notify.Record[]
function notify.history(opts)
if not global_instance then
notify.setup()
end
return global_instance.history(opts)
end
---@class notify.HistoryOpts
---@field include_hidden boolean Include notifications hidden from history
--- Dismiss all notification windows currently displayed
---@param opts notify.DismissOpts
function notify.dismiss(opts)
if not global_instance then
notify.setup()
end
return global_instance.dismiss(opts)
end
---@class notify.DismissOpts
---@field pending boolean Clear pending notifications
---@field silent boolean Suppress notification that pending notifications were dismissed.
--- Open a notification in a new buffer
---@param notif_id integer|notify.Record
---@param opts notify.OpenOpts
---@return notify.OpenedBuffer
function notify.open(notif_id, opts)
if not global_instance then
notify.setup()
end
return global_instance.open(notif_id, opts)
end
---@class notify.OpenOpts
---@field buffer integer Use this buffer, instead of creating a new one
---@field max_width integer Render message to this width (used to limit window decoration sizes)
---@class notify.OpenedBuffer
---@field buffer integer Created buffer number
---@field height integer Height of the buffer content including extmarks
---@field width integer width of the buffer content including extmarks
---@field highlights table<string, string> Highlights used for the buffer contents
--- Number of notifications currently waiting to be displayed
---@return integer[]
function notify.pending()
if not global_instance then
notify.setup()
end
return global_instance.pending()
end
function notify._print_history()
if not global_instance then
notify.setup()
end
for _, notif in ipairs(global_instance.history()) do
vim.api.nvim_echo({
{
vim.fn.strftime(notify._config().time_formats().notification_history, notif.time),
"NotifyLogTime",
},
{ " ", "MsgArea" },
{ notif.title[1], "NotifyLogTitle" },
{ #notif.title[1] > 0 and " " or "", "MsgArea" },
{ notif.icon, "Notify" .. notif.level .. "Title" },
{ " ", "MsgArea" },
{ notif.level, "Notify" .. notif.level .. "Title" },
{ " ", "MsgArea" },
{ table.concat(notif.message, "\n"), "MsgArea" },
}, false, {})
end
end
--- Configure an instance of nvim-notify.
--- You can use this to manage a separate instance of nvim-notify with completely different configuration.
--- The returned instance will have the same functions as the notify module.
---@param user_config notify.Config
---@param inherit? boolean Inherit the global configuration, default true
function notify.instance(user_config, inherit)
return instance(user_config, inherit, global_config)
end
setmetatable(notify, {
__call = function(_, m, l, o)
if vim.in_fast_event() or vim.fn.has("vim_starting") == 1 then
vim.schedule(function()
notify.notify(m, l, o)
end)
else
return notify.notify(m, l, o)
end
end,
})
return notify

View File

@ -0,0 +1,169 @@
local stages = require("notify.stages")
local config = require("notify.config")
local Notification = require("notify.service.notification")
local WindowAnimator = require("notify.windows")
local NotificationService = require("notify.service")
local NotificationBuf = require("notify.service.buffer")
local stage_util = require("notify.stages.util")
---@param user_config notify.Config
---@param inherit? boolean Inherit the global configuration, default true
---@param global_config notify.Config
return function(user_config, inherit, global_config)
---@type notify.Notification[]
local notifications = {}
user_config = user_config or {}
if inherit ~= false and global_config then
user_config = vim.tbl_deep_extend("force", global_config, user_config)
end
local instance_config = config.setup(user_config)
local animator_stages = instance_config.stages()
local direction = instance_config.top_down() and stage_util.DIRECTION.TOP_DOWN
or stage_util.DIRECTION.BOTTOM_UP
animator_stages = type(animator_stages) == "string" and stages[animator_stages](direction)
or animator_stages
local animator = WindowAnimator(animator_stages, instance_config)
local service = NotificationService(instance_config, animator)
local instance = {}
local function get_render(render)
if type(render) == "function" then
return render
end
return require("notify.render")[render]
end
function instance.notify(message, level, opts)
opts = opts or {}
if opts.replace then
if type(opts.replace) == "table" then
opts.replace = opts.replace.id
end
local existing = notifications[opts.replace]
if not existing then
vim.notify("Invalid notification to replace", "error", { title = "nvim-notify" })
return
end
local notif_keys = {
"title",
"icon",
"timeout",
"keep",
"on_open",
"on_close",
"render",
"hide_from_history",
"animate",
}
message = message or existing.message
level = level or existing.level
for _, key in ipairs(notif_keys) do
opts[key] = opts[key] or existing[key]
end
end
opts.render = get_render(opts.render or instance_config.render())
local id = #notifications + 1
local notification = Notification(id, message, level, opts, instance_config)
table.insert(notifications, notification)
local level_num = vim.log.levels[notification.level]
if opts.replace then
service:replace(opts.replace, notification)
elseif not level_num or level_num >= instance_config.level() then
service:push(notification)
end
return {
id = id,
}
end
---@param notif_id integer|notify.Record
---@param opts table
function instance.open(notif_id, opts)
opts = opts or {}
if type(notif_id) == "table" then
notif_id = notif_id.id
end
local notif = notifications[notif_id]
if not notif then
vim.notify(
"Invalid notification id: " .. notif_id,
vim.log.levels.WARN,
{ title = "nvim-notify" }
)
return
end
local buf = opts.buffer or vim.api.nvim_create_buf(false, true)
local notif_buf =
NotificationBuf(buf, notif, vim.tbl_extend("keep", opts, { config = instance_config }))
notif_buf:render()
return {
buffer = buf,
height = notif_buf:height(),
width = notif_buf:width(),
highlights = {
body = notif_buf.highlights.body,
border = notif_buf.highlights.border,
title = notif_buf.highlights.title,
icon = notif_buf.highlights.icon,
},
}
end
function instance.async(message, level, opts)
opts = opts or {}
local async = require("plenary.async")
local send_close, wait_close = async.control.channel.oneshot()
opts.on_close = send_close
local send_open, wait_open = async.control.channel.oneshot()
opts.on_open = send_open
async.util.scheduler()
local record = instance.notify(message, level, opts)
return vim.tbl_extend("error", record, {
events = {
open = wait_open,
close = wait_close,
},
})
end
function instance.history(args)
args = args or {}
local records = {}
for _, notif in ipairs(notifications) do
if not notif.hide_from_history or args.include_hidden then
records[#records + 1] = notif:record()
end
end
return records
end
function instance.dismiss(opts)
if service then
service:dismiss(opts or {})
end
end
function instance.pending()
return service and service:pending() or {}
end
setmetatable(instance, {
__call = function(_, m, l, o)
if vim.in_fast_event() then
vim.schedule(function()
instance.notify(m, l, o)
end)
else
return instance.notify(m, l, o)
end
end,
})
return instance, instance_config.merged()
end

View File

@ -0,0 +1,9 @@
local M = {}
local namespace = vim.api.nvim_create_namespace("nvim-notify")
function M.namespace()
return namespace
end
return M

View File

@ -0,0 +1,36 @@
local base = require("notify.render.base")
return function(bufnr, notif, highlights)
local namespace = base.namespace()
local icon = notif.icon
local title = notif.title[1]
local prefix
if type(title) == "string" and #title > 0 then
prefix = string.format("%s | %s:", icon, title)
else
prefix = string.format("%s |", icon)
end
notif.message[1] = string.format("%s %s", prefix, notif.message[1])
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, notif.message)
local icon_length = vim.str_utfindex(icon)
local prefix_length = vim.str_utfindex(prefix)
vim.api.nvim_buf_set_extmark(bufnr, namespace, 0, 0, {
hl_group = highlights.icon,
end_col = icon_length + 1,
priority = 50,
})
vim.api.nvim_buf_set_extmark(bufnr, namespace, 0, icon_length + 1, {
hl_group = highlights.title,
end_col = prefix_length + 1,
priority = 50,
})
vim.api.nvim_buf_set_extmark(bufnr, namespace, 0, prefix_length + 1, {
hl_group = highlights.body,
end_line = #notif.message,
priority = 50,
})
end

View File

@ -0,0 +1,54 @@
local api = vim.api
local base = require("notify.render.base")
return function(bufnr, notif, highlights, config)
local left_icon = notif.icon .. " "
local max_message_width = math.max(math.max(unpack(vim.tbl_map(function(line)
return vim.fn.strchars(line)
end, notif.message))))
local right_title = notif.title[2]
local left_title = notif.title[1]
local title_accum = vim.str_utfindex(left_icon)
+ vim.str_utfindex(right_title)
+ vim.str_utfindex(left_title)
local left_buffer = string.rep(" ", math.max(0, max_message_width - title_accum))
local namespace = base.namespace()
api.nvim_buf_set_lines(bufnr, 0, 1, false, { "", "" })
api.nvim_buf_set_extmark(bufnr, namespace, 0, 0, {
virt_text = {
{ " " },
{ left_icon, highlights.icon },
{ left_title .. left_buffer, highlights.title },
},
virt_text_win_col = 0,
priority = 10,
})
api.nvim_buf_set_extmark(bufnr, namespace, 0, 0, {
virt_text = { { " " }, { right_title, highlights.title }, { " " } },
virt_text_pos = "right_align",
priority = 10,
})
api.nvim_buf_set_extmark(bufnr, namespace, 1, 0, {
virt_text = {
{
string.rep(
"",
math.max(vim.str_utfindex(left_buffer) + title_accum + 2, config.minimum_width())
),
highlights.border,
},
},
virt_text_win_col = 0,
priority = 10,
})
api.nvim_buf_set_lines(bufnr, 2, -1, false, notif.message)
api.nvim_buf_set_extmark(bufnr, namespace, 2, 0, {
hl_group = highlights.body,
end_line = 1 + #notif.message,
end_col = #notif.message[#notif.message],
priority = 50, -- Allow treesitter to override
})
end

View File

@ -0,0 +1,38 @@
---@tag notify-render
---@text
--- Notification buffer rendering
---
--- Custom rendering can be provided by both the user config in the setup or on
--- an individual notification using the `render` key.
--- The key can either be the name of a built-in renderer or a custom function.
---
--- Built-in renderers:
--- - `"default"`
--- - `"minimal"`
--- - `"simple"`
--- - `"compact"`
--- - `"wrapped-compact"`
---
--- Custom functions should accept a buffer, a notification record and a highlights table
---
--- >
--- render: fun(buf: integer, notification: notify.Record, highlights: notify.Highlights, config)
--- <
--- You should use the provided highlight groups to take advantage of opacity
--- changes as they will be updated as the notification is animated
---@class notify.Highlights
---@field title string
---@field icon string
---@field border string
---@field body string
local M = {}
setmetatable(M, {
__index = function(_, key)
return require("notify.render." .. key)
end,
})
return M

View File

@ -0,0 +1,14 @@
local api = vim.api
local base = require("notify.render.base")
return function(bufnr, notif, highlights)
local namespace = base.namespace()
api.nvim_buf_set_lines(bufnr, 0, -1, false, notif.message)
api.nvim_buf_set_extmark(bufnr, namespace, 0, 0, {
hl_group = highlights.icon,
end_line = #notif.message - 1,
end_col = #notif.message[#notif.message],
priority = 50,
})
end

View File

@ -0,0 +1,44 @@
local api = vim.api
local base = require("notify.render.base")
return function(bufnr, notif, highlights, config)
local max_message_width = math.max(math.max(unpack(vim.tbl_map(function(line)
return vim.fn.strchars(line)
end, notif.message))))
local title = notif.title[1]
local title_accum = vim.str_utfindex(title)
local title_buffer = string.rep(
" ",
(math.max(max_message_width, title_accum, config.minimum_width()) - title_accum) / 2
)
local namespace = base.namespace()
api.nvim_buf_set_lines(bufnr, 0, 1, false, { "", "" })
api.nvim_buf_set_extmark(bufnr, namespace, 0, 0, {
virt_text = {
{ title_buffer .. title .. title_buffer, highlights.title },
},
virt_text_win_col = 0,
priority = 10,
})
api.nvim_buf_set_extmark(bufnr, namespace, 1, 0, {
virt_text = {
{
string.rep("", math.max(max_message_width, title_accum, config.minimum_width())),
highlights.border,
},
},
virt_text_win_col = 0,
priority = 10,
})
api.nvim_buf_set_lines(bufnr, 2, -1, false, notif.message)
api.nvim_buf_set_extmark(bufnr, namespace, 2, 0, {
hl_group = highlights.body,
end_line = 1 + #notif.message,
end_col = #notif.message[#notif.message],
priority = 50,
})
end

View File

@ -0,0 +1,89 @@
-- alternative compact renderer for nvim-notify.
-- Wraps text and adds some padding (only really to the left, since padding to
-- the right is somehow not display correctly).
-- Modified version of https://github.com/rcarriga/nvim-notify/blob/master/lua/notify/render/compact.lua
--------------------------------------------------------------------------------
---@param line string
---@param width number
---@return string[]
local function split_length(line, width)
local text = {}
local next_line
while true do
if #line == 0 then
return text
end
next_line, line = line:sub(1, width), line:sub(width)
text[#text + 1] = next_line
end
end
---@param lines string[]
---@param max_width number
---@return string[]
local function custom_wrap(lines, max_width)
local wrapped_lines = {}
for _, line in pairs(lines) do
local new_lines = split_length(line, max_width)
for _, nl in ipairs(new_lines) do
nl = nl:gsub("^%s*", " "):gsub("%s*$", " ") -- ensure padding
table.insert(wrapped_lines, nl)
end
end
return wrapped_lines
end
---@param bufnr number
---@param notif object
---@param highlights object
---@param config object plugin config_obj
return function(bufnr, notif, highlights, config)
local namespace = require("notify.render.base").namespace()
local icon = notif.icon
local title = notif.title[1]
local prefix
-- wrap the text & add spacing
local max_width = config.max_width()
if max_width == nil then
max_width = 80
end
local message = custom_wrap(notif.message, max_width)
local default_titles = { "Error", "Warning", "Notify" }
local has_valid_manual_title = type(title) == "string"
and #title > 0
and not vim.tbl_contains(default_titles, title)
if has_valid_manual_title then
-- has title = icon + title as header row
prefix = string.format(" %s %s", icon, title)
table.insert(message, 1, prefix)
else
-- no title = prefix the icon
prefix = string.format(" %s", icon)
message[1] = string.format("%s %s", prefix, message[1])
end
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, message)
local icon_length = vim.str_utfindex(icon)
local prefix_length = vim.str_utfindex(prefix) + 1
vim.api.nvim_buf_set_extmark(bufnr, namespace, 0, 0, {
hl_group = highlights.icon,
end_col = icon_length + 1,
priority = 50,
})
vim.api.nvim_buf_set_extmark(bufnr, namespace, 0, icon_length + 1, {
hl_group = highlights.title,
end_col = prefix_length + 1,
priority = 50,
})
vim.api.nvim_buf_set_extmark(bufnr, namespace, 0, prefix_length + 1, {
hl_group = highlights.body,
end_line = #message,
priority = 50,
})
end

View File

@ -0,0 +1,187 @@
local util = require("notify.util")
---@class NotifyBufHighlights
---@field groups table
---@field opacity number
---@field title string
---@field border string
---@field icon string
---@field body string
---@field buffer number
---@field _config table
local NotifyBufHighlights = {}
local function manual_get_hl(name)
local synID = vim.fn.synIDtrans(vim.fn.hlID(name))
local result = {
foreground = tonumber(vim.fn.synIDattr(synID, "fg"):gsub("#", ""), 16),
background = tonumber(vim.fn.synIDattr(synID, "bg"):gsub("#", ""), 16),
}
return result
end
local function get_hl(name)
local definition = vim.api.nvim_get_hl_by_name(name, true)
if definition[true] then
-- https://github.com/neovim/neovim/issues/18024
return manual_get_hl(name)
end
return definition
end
function NotifyBufHighlights:new(level, buffer, config)
local function linked_group(section)
local orig = "Notify" .. level .. section
if vim.fn.hlID(orig) == 0 then
orig = "NotifyINFO" .. section
end
local new = orig .. buffer
vim.api.nvim_set_hl(0, new, { link = orig })
return new, get_hl(new)
end
local title, title_def = linked_group("Title")
local border, border_def = linked_group("Border")
local body, body_def = linked_group("Body")
local icon, icon_def = linked_group("Icon")
local groups = {
[title] = title_def,
[border] = border_def,
[body] = body_def,
[icon] = icon_def,
}
local buf_highlights = {
groups = groups,
opacity = 100,
border = border,
body = body,
title = title,
icon = icon,
buffer = buffer,
_config = config,
}
self.__index = self
setmetatable(buf_highlights, self)
return buf_highlights
end
function NotifyBufHighlights:_redefine_treesitter()
local buf_highlighter = require("vim.treesitter.highlighter").active[self.buffer]
if not buf_highlighter then
return
end
local render_namespace = vim.api.nvim_create_namespace("notify-treesitter-override")
vim.api.nvim_buf_clear_namespace(self.buffer, render_namespace, 0, -1)
local function link(orig)
local new = orig .. self.buffer
if self.groups[new] then
return new
end
vim.api.nvim_set_hl(0, new, { link = orig })
self.groups[new] = get_hl(new)
return new
end
local matches = {}
local i = 0
buf_highlighter.tree:for_each_tree(function(tstree, tree)
if not tstree then
return
end
local root = tstree:root()
local query = buf_highlighter:get_query(tree:lang())
-- Some injected languages may not have highlight queries.
if not query:query() then
return
end
local iter = query:query():iter_captures(root, buf_highlighter.bufnr)
for capture, node, metadata in iter do
-- Wait until we get at least a single capture as we don't know when parsing is complete.
self._treesitter_redefined = true
local hl = query.hl_cache[capture]
if hl then
i = i + 1
local c = query._query.captures[capture] -- name of the capture in the query
if c ~= nil then
local capture_hl
-- Removed in nightly with change of highlight names to @...
-- https://github.com/neovim/neovim/pull/19931
if query._get_hl_from_capture then
local general_hl, is_vim_hl = query:_get_hl_from_capture(capture)
capture_hl = is_vim_hl and general_hl or (tree:lang() .. general_hl)
else
capture_hl = query._query.captures[capture]
if not vim.startswith(capture_hl, "_") then
capture_hl = "@" .. capture_hl .. "." .. tree:lang()
end
end
local start_row, start_col, end_row, end_col = node:range()
local custom_hl = link(capture_hl)
vim.api.nvim_buf_set_extmark(self.buffer, render_namespace, start_row, start_col, {
end_row = end_row,
end_col = end_col,
hl_group = custom_hl,
-- TODO: Not sure how neovim's highlighter doesn't have issues with overriding highlights
-- Three marks on same region always show the second for some reason AFAICT
priority = metadata.priority or i + 200,
conceal = metadata.conceal,
})
end
end
end
end, true)
return matches
end
function NotifyBufHighlights:set_opacity(alpha)
if
not self._treesitter_redefined
and vim.api.nvim_buf_get_option(self.buffer, "filetype") ~= "notify"
then
self:_redefine_treesitter()
end
self.opacity = alpha
local background = self._config.background_colour()
for group, fields in pairs(self.groups) do
local updated_fields = {}
vim.api.nvim_set_hl(0, group, updated_fields)
local hl_string = ""
if fields.foreground then
hl_string = "guifg=#"
.. string.format("%06x", util.blend(fields.foreground, background, alpha / 100))
end
if fields.background then
hl_string = hl_string
.. " guibg=#"
.. string.format("%06x", util.blend(fields.background, background, alpha / 100))
end
if hl_string ~= "" then
-- Can't use nvim_set_hl https://github.com/neovim/neovim/issues/18160
vim.cmd("hi " .. group .. " " .. hl_string)
end
end
end
function NotifyBufHighlights:get_opacity()
return self.opacity
end
---@return NotifyBufHighlights
return function(level, buffer, config)
return NotifyBufHighlights:new(level, buffer, config)
end

View File

@ -0,0 +1,159 @@
local api = vim.api
local NotifyBufHighlights = require("notify.service.buffer.highlights")
---@class NotificationBuf
---@field highlights NotifyBufHighlights
---@field _config table
---@field _notif notify.Notification
---@field _state "open" | "closed"
---@field _buffer number
---@field _height number
---@field _width number
---@field _max_width number | nil
local NotificationBuf = {}
local BufState = {
OPEN = "open",
CLOSED = "close",
}
function NotificationBuf:new(kwargs)
local notif_buf = {
_config = kwargs.config,
_max_width = kwargs.max_width,
_buffer = kwargs.buffer,
_state = BufState.CLOSED,
_width = 0,
_height = 0,
}
setmetatable(notif_buf, self)
self.__index = self
notif_buf:set_notification(kwargs.notif)
return notif_buf
end
function NotificationBuf:set_notification(notif)
self._notif = notif
self:_create_highlights()
end
function NotificationBuf:_create_highlights()
local existing_opacity = self.highlights and self.highlights.opacity or 100
self.highlights = NotifyBufHighlights(self._notif.level, self._buffer, self._config)
if existing_opacity < 100 then
self.highlights:set_opacity(existing_opacity)
end
end
function NotificationBuf:open(win)
if self._state ~= BufState.CLOSED then
return
end
self._state = BufState.OPEN
local record = self._notif:record()
if self._notif.on_open then
self._notif.on_open(win, record)
end
if self._config.on_open() then
self._config.on_open()(win, record)
end
end
function NotificationBuf:should_animate()
return self._notif.animate
end
function NotificationBuf:close(win)
if self._state ~= BufState.OPEN then
return
end
self._state = BufState.CLOSED
vim.schedule(function()
if self._notif.on_close then
self._notif.on_close(win)
end
if self._config.on_close() then
self._config.on_close()(win)
end
pcall(api.nvim_buf_delete, self._buffer, { force = true })
end)
end
function NotificationBuf:height()
return self._height
end
function NotificationBuf:width()
return self._width
end
function NotificationBuf:should_stay()
if self._notif.keep then
return self._notif.keep()
end
return false
end
function NotificationBuf:render()
local notif = self._notif
local buf = self._buffer
local render_namespace = require("notify.render.base").namespace()
api.nvim_buf_set_option(buf, "filetype", "notify")
api.nvim_buf_set_option(buf, "modifiable", true)
api.nvim_buf_clear_namespace(buf, render_namespace, 0, -1)
notif.render(buf, notif, self.highlights, self._config)
api.nvim_buf_set_option(buf, "modifiable", false)
local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
local width = self._config.minimum_width()
for _, line in pairs(lines) do
width = math.max(width, vim.str_utfindex(line))
end
local success, extmarks =
pcall(api.nvim_buf_get_extmarks, buf, render_namespace, 0, #lines, { details = true })
if not success then
extmarks = {}
end
local virt_texts = {}
for _, mark in ipairs(extmarks) do
local details = mark[4]
for _, virt_text in ipairs(details.virt_text or {}) do
virt_texts[mark[2]] = (virt_texts[mark[2]] or "") .. virt_text[1]
end
end
for _, text in pairs(virt_texts) do
width = math.max(width, vim.str_utfindex(text))
end
self._width = width
self._height = #lines
end
function NotificationBuf:timeout()
return self._notif.timeout
end
function NotificationBuf:buffer()
return self._buffer
end
function NotificationBuf:is_valid()
return self._buffer and vim.api.nvim_buf_is_valid(self._buffer)
end
function NotificationBuf:level()
return self._notif.level
end
---@param buf number
---@param notification notify.Notification;q
---@return NotificationBuf
return function(buf, notification, opts)
return NotificationBuf:new(
vim.tbl_extend("keep", { buffer = buf, notif = notification }, opts or {})
)
end

View File

@ -0,0 +1,116 @@
local util = require("notify.util")
local NotificationBuf = require("notify.service.buffer")
---@class NotificationService
---@field private _running boolean
---@field private _pending FIFOQueue
---@field private _animator WindowAnimator
---@field private _buffers table<integer, NotificationBuf>
---@field private _fps integer
local NotificationService = {}
---@class notify.ServiceConfig
---@field fps integer
---@param config notify.ServiceConfig
function NotificationService:new(config, animator)
local service = {
_config = config,
_fps = config.fps(),
_animator = animator,
_pending = util.FIFOQueue(),
_running = false,
_buffers = {},
}
self.__index = self
setmetatable(service, self)
return service
end
function NotificationService:_run()
self._running = true
local succees, updated =
pcall(self._animator.render, self._animator, self._pending, 1 / self._fps)
if not succees then
print("Error running notification service: " .. updated)
self._running = false
return
end
if not updated then
self._running = false
return
end
vim.defer_fn(function()
self:_run()
end, 1000 / self._fps)
end
---@param notif notify.Notification;q
---@return integer
function NotificationService:push(notif)
local buf = vim.api.nvim_create_buf(false, true)
local notif_buf = NotificationBuf(buf, notif, { config = self._config })
notif_buf:render()
self._buffers[notif.id] = notif_buf
self._pending:push(notif_buf)
if not self._running then
self:_run()
else
-- Forces a render during blocking events
-- https://github.com/rcarriga/nvim-notify/issues/5
pcall(self._animator.render, self._animator, self._pending, 1 / self._fps)
end
vim.cmd("redraw")
return buf
end
function NotificationService:replace(id, notif)
local existing = self._buffers[id]
if not (existing and existing:is_valid()) then
vim.notify("No matching notification found to replace")
return
end
existing:set_notification(notif)
self._buffers[id] = nil
self._buffers[notif.id] = existing
pcall(existing.render, existing)
local win = vim.fn.bufwinid(existing:buffer())
if win ~= -1 then
-- Highlights can change name if level changed so we have to re-link
-- vim.wo does not behave like setlocal, thus we use setwinvar to set a
-- local option. Otherwise our changes would affect subsequently opened
-- windows.
-- see e.g. neovim#14595
vim.fn.setwinvar(
win,
"&winhl",
"Normal:" .. existing.highlights.body .. ",FloatBorder:" .. existing.highlights.border
)
self._animator:on_refresh(win)
end
end
function NotificationService:dismiss(opts)
local notif_wins = vim.tbl_keys(self._animator.win_stages)
for _, win in pairs(notif_wins) do
pcall(vim.api.nvim_win_close, win, true)
end
if opts.pending then
local cleared = 0
while self._pending:pop() do
cleared = cleared + 1
end
if not opts.silent then
vim.notify("Cleared " .. cleared .. " pending notifications")
end
end
end
function NotificationService:pending()
return self._pending:length()
end
---@return NotificationService
return function(config, animator)
return NotificationService:new(config, animator)
end

View File

@ -0,0 +1,78 @@
---@class notify.Notification
---@field id integer
---@field level string
---@field message string[]
---@field timeout number | nil
---@field title string[]
---@field icon string
---@field time number
---@field width number
---@field animate boolean
---@field hide_from_history boolean
---@field keep fun(): boolean
---@field on_open fun(win: number, record: notify.Record) | nil
---@field on_close fun(win: number, record: notify.Record) | nil
---@field render fun(buf: integer, notification: notify.Notification, highlights: table<string, string>)
local Notification = {}
local level_maps = vim.tbl_extend("keep", {}, vim.log.levels)
for k, v in pairs(vim.log.levels) do
level_maps[v] = k
end
function Notification:new(id, message, level, opts, config)
if type(level) == "number" then
level = level_maps[level]
end
if type(message) == "string" then
message = vim.split(message, "\n")
end
level = vim.fn.toupper(level or "info")
local time = vim.fn.localtime()
local title = opts.title or ""
if type(title) == "string" then
title = { title, vim.fn.strftime(config.time_formats().notification, time) }
end
vim.validate({
message = { message, "table" },
level = { level, "string" },
title = { title, "table" },
})
local notif = {
id = id,
message = message,
title = title,
icon = opts.icon or config.icons()[level] or config.icons().INFO,
time = time,
timeout = opts.timeout,
level = level,
keep = opts.keep,
on_open = opts.on_open,
on_close = opts.on_close,
animate = opts.animate ~= false,
render = opts.render,
hide_from_history = opts.hide_from_history,
}
self.__index = self
setmetatable(notif, self)
return notif
end
function Notification:record()
return {
id = self.id,
message = self.message,
level = self.level,
time = self.time,
title = self.title,
icon = self.icon,
render = self.render,
}
end
---@param message string | string[]
---@param level string | number
---@param opts notify.Options
return function(id, message, level, opts, config)
return Notification:new(id, message, level, opts, config)
end

View File

@ -0,0 +1,48 @@
local stages_util = require("notify.stages.util")
return function(direction)
return {
function(state)
local next_height = state.message.height + 2
local next_row = stages_util.available_slot(state.open_windows, next_height, direction)
if not next_row then
return nil
end
return {
relative = "editor",
anchor = "NE",
width = state.message.width,
height = state.message.height,
col = vim.opt.columns:get(),
row = next_row,
border = "rounded",
style = "minimal",
opacity = 0,
}
end,
function()
return {
opacity = { 100 },
col = { vim.opt.columns:get() },
}
end,
function()
return {
col = { vim.opt.columns:get() },
time = true,
}
end,
function()
return {
opacity = {
0,
frequency = 2,
complete = function(cur_opacity)
return cur_opacity <= 4
end,
},
col = { vim.opt.columns:get() },
}
end,
}
end

View File

@ -0,0 +1,77 @@
local stages_util = require("notify.stages.util")
return function(direction)
return {
function(state)
local next_height = state.message.height + 2
local next_row = stages_util.available_slot(state.open_windows, next_height, direction)
if not next_row then
return nil
end
return {
relative = "editor",
anchor = "NE",
width = state.message.width,
height = state.message.height,
col = vim.opt.columns:get(),
row = next_row,
border = "rounded",
style = "minimal",
opacity = 0,
}
end,
function(state, win)
return {
opacity = { 100 },
col = { vim.opt.columns:get() },
row = {
stages_util.slot_after_previous(win, state.open_windows, direction),
frequency = 3,
complete = function()
return true
end,
},
}
end,
function(state, win)
return {
col = { vim.opt.columns:get() },
time = true,
row = {
stages_util.slot_after_previous(win, state.open_windows, direction),
frequency = 3,
complete = function()
return true
end,
},
}
end,
function(state, win)
return {
width = {
1,
frequency = 2.5,
damping = 0.9,
complete = function(cur_width)
return cur_width < 3
end,
},
opacity = {
0,
frequency = 2,
complete = function(cur_opacity)
return cur_opacity <= 4
end,
},
col = { vim.opt.columns:get() },
row = {
stages_util.slot_after_previous(win, state.open_windows, direction),
frequency = 3,
complete = function()
return true
end,
},
}
end,
}
end

View File

@ -0,0 +1,20 @@
local M = {}
---@class MessageState
---@field width number
---@field height number
---@alias InitStage fun(open_windows: number[], message_state: MessageState): table | nil
---@alias AnimationStage fun(win: number, message_state: MessageState): table
---@alias Stage InitStage | AnimationStage
---@alias Stages Stage[]
setmetatable(M, {
---@return Stages
__index = function(_, key)
return require("notify.stages." .. key)
end,
})
return M

View File

@ -0,0 +1,30 @@
local stages_util = require("notify.stages.util")
return function(direction)
return {
function(state)
local next_height = state.message.height + 2
local next_row = stages_util.available_slot(state.open_windows, next_height, direction)
if not next_row then
return nil
end
return {
relative = "editor",
anchor = "NE",
width = state.message.width,
height = state.message.height,
col = vim.opt.columns:get(),
row = next_row,
border = "rounded",
style = "minimal",
}
end,
function(state, win)
return {
col = vim.opt.columns:get(),
time = true,
row = stages_util.slot_after_previous(win, state.open_windows, direction),
}
end,
}
end

View File

@ -0,0 +1,48 @@
local stages_util = require("notify.stages.util")
return function(direction)
return {
function(state)
local next_height = state.message.height + 2
local next_row = stages_util.available_slot(state.open_windows, next_height, direction)
if not next_row then
return nil
end
return {
relative = "editor",
anchor = "NE",
width = 1,
height = state.message.height,
col = vim.opt.columns:get(),
row = next_row,
border = "rounded",
style = "minimal",
}
end,
function(state)
return {
width = { state.message.width, frequency = 2 },
col = { vim.opt.columns:get() },
}
end,
function()
return {
col = { vim.opt.columns:get() },
time = true,
}
end,
function()
return {
width = {
1,
frequency = 2.5,
damping = 0.9,
complete = function(cur_width)
return cur_width < 2
end,
},
col = { vim.opt.columns:get() },
}
end,
}
end

View File

@ -0,0 +1,29 @@
local stages_util = require("notify.stages.util")
return function(direction)
return {
function(state)
local next_height = state.message.height + 2
local next_row = stages_util.available_slot(state.open_windows, next_height, direction)
if not next_row then
return nil
end
return {
relative = "editor",
anchor = "NE",
width = state.message.width,
height = state.message.height,
col = vim.opt.columns:get(),
row = next_row,
border = "rounded",
style = "minimal",
}
end,
function()
return {
col = vim.opt.columns:get(),
time = true,
}
end,
}
end

View File

@ -0,0 +1,197 @@
local max, min = math.max, math.min
local util = require("notify.util")
local M = {}
M.DIRECTION = {
TOP_DOWN = "top_down",
BOTTOM_UP = "bottom_up",
LEFT_RIGHT = "left_right",
RIGHT_LEFT = "right_left",
}
local function is_increasing(direction)
return (direction == M.DIRECTION.TOP_DOWN or direction == M.DIRECTION.LEFT_RIGHT)
end
local function moves_vertically(direction)
return (direction == M.DIRECTION.TOP_DOWN or direction == M.DIRECTION.BOTTOM_UP)
end
function M.slot_name(direction)
if moves_vertically(direction) then
return "height"
end
return "width"
end
local function less(a, b)
return a < b
end
local function greater(a, b)
return a > b
end
local function overlaps(a, b)
return a.min <= b.max and b.min <= a.max
end
local move_slot = function(direction, slot, delta)
if is_increasing(direction) then
return slot + delta
end
return slot - delta
end
local function slot_key(direction)
return moves_vertically(direction) and "row" or "col"
end
local function space_key(direction)
return moves_vertically(direction) and "height" or "width"
end
-- TODO: Use direction to check border lists
local function border_padding(direction, win_conf)
if not win_conf.border or win_conf.border == "none" then
return 0
end
return 2
end
---@param windows number[]
---@param direction string
---@return { max: integer, min: integer}[]
local function window_intervals(windows, direction, cmp)
local win_intervals = {}
for _, w in ipairs(windows) do
local exists, existing_conf = util.get_win_config(w)
if exists then
local border_space = border_padding(direction, existing_conf)
win_intervals[#win_intervals + 1] = {
min = existing_conf[slot_key(direction)],
max = existing_conf[slot_key(direction)]
+ existing_conf[space_key(direction)]
+ border_space
- 1,
}
end
end
table.sort(win_intervals, function(a, b)
return cmp(a.min, b.min)
end)
return win_intervals
end
function M.get_slot_range(direction)
local top = vim.opt.tabline:get() == "" and 0 or 1
local bottom = vim.opt.lines:get()
- (vim.opt.cmdheight:get() + (vim.opt.laststatus:get() > 0 and 1 or 0))
local left = 1
local right = vim.opt.columns:get()
if M.DIRECTION.TOP_DOWN == direction then
return top, bottom
elseif M.DIRECTION.BOTTOM_UP == direction then
return bottom, top
elseif M.DIRECTION.LEFT_RIGHT == direction then
return left, right
elseif M.DIRECTION.RIGHT_LEFT == direction then
return right, left
end
error(string.format("Invalid direction: %s", direction))
end
---@param existing_wins number[] Windows to avoid overlapping
---@param required_space number Window height or width including borders
---@param direction string Direction to stack windows, one of M.DIRECTION
---@return number | nil Slot to place window at or nil if no slot available
function M.available_slot(existing_wins, required_space, direction)
local increasing = is_increasing(direction)
local cmp = increasing and less or greater
local first_slot, last_slot = M.get_slot_range(direction)
local function create_interval(start_slot)
local end_slot = move_slot(direction, start_slot, required_space - 1)
return { min = min(start_slot, end_slot), max = max(start_slot, end_slot) }
end
local interval = create_interval(first_slot)
local intervals = window_intervals(existing_wins, direction, cmp)
for _, next_interval in ipairs(intervals) do
if overlaps(next_interval, interval) then
interval = create_interval(
move_slot(direction, increasing and next_interval.max or next_interval.min, 1)
)
end
end
if #intervals > 0 and not cmp(is_increasing and interval.max or interval.min, last_slot) then
return nil
end
return interval.min
end
---Gets the next slot available for the given window while maintaining its position using the given list.
---@param win number
---@param open_windows number[]
---@param direction string
function M.slot_after_previous(win, open_windows, direction)
local key = slot_key(direction)
local cmp = is_increasing(direction) and less or greater
local exists, cur_win_conf = util.get_win_config(win)
if not exists then
return 0
end
local cur_slot = cur_win_conf[key]
local win_confs = {}
for _, w in ipairs(open_windows) do
local success, conf = util.get_win_config(w)
if success then
win_confs[w] = conf
end
end
local preceding_wins = vim.tbl_filter(function(open_win)
return win_confs[open_win] and cmp(win_confs[open_win][key], cur_slot)
end, open_windows)
if #preceding_wins == 0 then
local start = M.get_slot_range(direction)
if is_increasing(direction) then
return start
end
return move_slot(
direction,
start,
cur_win_conf[space_key(direction)] + border_padding(direction, cur_win_conf)
)
end
table.sort(preceding_wins, function(a, b)
return cmp(win_confs[a][key], win_confs[b][key])
end)
local last_win = preceding_wins[#preceding_wins]
local last_win_conf = win_confs[last_win]
if is_increasing(direction) then
return move_slot(
direction,
last_win_conf[key],
last_win_conf[space_key(direction)] + border_padding(direction, last_win_conf)
)
else
return move_slot(
direction,
last_win_conf[key],
cur_win_conf[space_key(direction)] + border_padding(direction, cur_win_conf)
)
end
end
return M

View File

@ -0,0 +1,120 @@
local M = {}
local min, max, floor = math.min, math.max, math.floor
local rshift, lshift, band, bor = bit.rshift, bit.lshift, bit.band, bit.bor
function M.is_callable(obj)
return type(obj) == "function" or (type(obj) == "table" and obj.__call)
end
function M.lazy_require(require_path)
return setmetatable({}, {
__call = function(_, ...)
return require(require_path)(...)
end,
__index = function(_, key)
return require(require_path)[key]
end,
__newindex = function(_, key, value)
require(require_path)[key] = value
end,
})
end
function M.pop(tbl, key, default)
local val = default
if tbl[key] then
val = tbl[key]
tbl[key] = nil
end
return val
end
function M.blend(fg_hex, bg_hex, alpha)
local segment = 0xFF0000
local result = 0
for i = 2, 0, -1 do
local blended = alpha * rshift(band(fg_hex, segment), i * 8)
+ (1 - alpha) * rshift(band(bg_hex, segment), i * 8)
result = bor(lshift(result, 8), floor((min(max(blended, 0), 255)) + 0.5))
segment = rshift(segment, 8)
end
return result
end
function M.round(num, decimals)
if decimals then
return tonumber(string.format("%." .. decimals .. "f", num))
end
return math.floor(num + 0.5)
end
function M.partial(func, ...)
local args = { ... }
return function(...)
local final = {}
vim.list_extend(final, args)
vim.list_extend(final, { ... })
return func(unpack(final))
end
end
function M.get_win_config(win)
local success, conf = pcall(vim.api.nvim_win_get_config, win)
if not success or not conf.row then
return false, conf
end
if type(conf.row) == "table" then
conf.row = conf.row[false]
end
if type(conf.col) == "table" then
conf.col = conf.col[false]
end
return success, conf
end
function M.open_win(notif_buf, enter, opts)
local win = vim.api.nvim_open_win(notif_buf:buffer(), enter, opts)
-- vim.wo does not behave like setlocal, thus we use setwinvar to set local
-- only options. Otherwise our changes would affect subsequently opened
-- windows.
-- see e.g. neovim#14595
vim.fn.setwinvar(
win,
"&winhl",
"Normal:" .. notif_buf.highlights.body .. ",FloatBorder:" .. notif_buf.highlights.border
)
vim.fn.setwinvar(win, "&wrap", 0)
return win
end
M.FIFOQueue = require("notify.util.queue")
function M.rgb_to_numbers(s)
local colours = {}
for a in string.gmatch(s, "[A-Fa-f0-9][A-Fa-f0-9]") do
colours[#colours + 1] = tonumber(a, 16)
end
return colours
end
function M.numbers_to_rgb(colours)
local colour = "#"
for _, num in pairs(colours) do
colour = colour .. string.format("%X", num)
end
return colour
end
function M.highlight(name, fields)
local fields_string = ""
for field, value in pairs(fields) do
fields_string = fields_string .. " " .. field .. "=" .. value
end
if fields_string ~= "" then
vim.cmd("hi " .. name .. fields_string)
end
end
return M

View File

@ -0,0 +1,54 @@
---@class FIFOQueue
local FIFOQueue = {}
function FIFOQueue:pop()
if self:is_empty() then
return nil
end
local r = self[self.pop_from]
self[self.pop_from] = nil
self.pop_from = self.pop_from - 1
return r
end
function FIFOQueue:peek()
return self[self.pop_from]
end
function FIFOQueue:push(val)
self[self.push_to] = val
self.push_to = self.push_to - 1
end
function FIFOQueue:is_empty()
return self:length() == 0
end
function FIFOQueue:length()
return self.pop_from - self.push_to
end
function FIFOQueue:iter()
local i = self.pop_from + 1
return function()
if i > self.push_to + 1 then
i = i - 1
return self[i]
end
end
end
function FIFOQueue:new()
local queue = {
pop_from = 1,
push_to = 1,
}
self.__index = self
setmetatable(queue, self)
return queue
end
---@return FIFOQueue
return function()
return FIFOQueue:new()
end

View File

@ -0,0 +1,326 @@
local api = vim.api
local animate = require("notify.animate")
local util = require("notify.util")
local round = util.round
local max = math.max
---@class WindowAnimator
---@field config table
---@field win_states table<number, table<string, SpringState>>
---@field win_stages table<number, integer>
---@field notif_bufs table<number, NotificationBuf>
---@field timers table
---@field stages table
local WindowAnimator = {}
function WindowAnimator:new(stages, config)
local animator = {
config = config,
win_stages = {},
win_states = {},
notif_bufs = {},
timers = {},
stages = stages,
}
self.__index = self
setmetatable(animator, self)
return animator
end
function WindowAnimator:render(queue, time)
self:push_pending(queue)
if vim.tbl_isempty(self.win_stages) then
return false
end
local open_windows = vim.tbl_keys(self.win_stages)
for win, _ in pairs(self.win_stages) do
self:_update_window(time, win, open_windows)
end
return true
end
function WindowAnimator:push_pending(queue)
if queue:is_empty() then
return
end
while not queue:is_empty() do
---@type NotificationBuf
local notif_buf = queue:peek()
if not notif_buf:is_valid() then
queue:pop()
else
local windows = vim.tbl_keys(self.win_stages)
local win_opts = self.stages[1]({
message = self:_get_dimensions(notif_buf),
open_windows = windows,
})
if not win_opts then
return
end
local opacity = util.pop(win_opts, "opacity")
if opacity then
notif_buf.highlights:set_opacity(opacity)
end
win_opts.noautocmd = true
local win = util.open_win(notif_buf, false, win_opts)
vim.fn.setwinvar(
win,
"&winhl",
"Normal:" .. notif_buf.highlights.body .. ",FloatBorder:" .. notif_buf.highlights.border
)
self.win_stages[win] = 2
self.win_states[win] = {}
self.notif_bufs[win] = notif_buf
queue:pop()
notif_buf:open(win)
end
end
end
function WindowAnimator:_advance_win_stage(win)
local cur_stage = self.win_stages[win]
if not cur_stage then
return
end
if cur_stage < #self.stages then
if api.nvim_get_current_win() == win then
return
end
self.win_stages[win] = cur_stage + 1
return
end
self.win_stages[win] = nil
local function close()
if api.nvim_get_current_win() == win then
return vim.defer_fn(close, 1000)
end
self:_remove_win(win)
end
close()
end
function WindowAnimator:_remove_win(win)
pcall(api.nvim_win_close, win, true)
self.win_stages[win] = nil
self.win_states[win] = nil
local notif_buf = self.notif_bufs[win]
self.notif_bufs[win] = nil
notif_buf:close(win)
end
function WindowAnimator:on_refresh(win)
local notif_buf = self.notif_bufs[win]
if not notif_buf then
return
end
if self.timers[win] then
self.timers[win]:set_repeat(notif_buf:timeout() or self.config.default_timeout())
self.timers[win]:again()
end
end
function WindowAnimator:_start_timer(win)
local buf_time = self.notif_bufs[win]:timeout() == nil and self.config.default_timeout()
or self.notif_bufs[win]:timeout()
if buf_time ~= false then
if buf_time == true then
buf_time = nil
end
local timer = vim.loop.new_timer()
self.timers[win] = timer
timer:start(
buf_time,
buf_time,
vim.schedule_wrap(function()
timer:stop()
self.timers[win] = nil
local notif_buf = self.notif_bufs[win]
if notif_buf and notif_buf:should_stay() then
return
end
self:_advance_win_stage(win)
end)
)
end
end
function WindowAnimator:_update_window(time, win, open_windows)
local stage = self.win_stages[win]
local notif_buf = self.notif_bufs[win]
local win_goals = self:_get_win_goals(win, stage, open_windows)
if not win_goals then
self:_remove_win(win)
end
-- If we don't animate, then we move to all goals instantly.
-- Can't just jump to the end, because we need to the intermediate changes
while
not notif_buf:should_animate()
and win_goals.time == nil
and self.win_stages[win] < #self.stages
do
for field, goal in pairs(win_goals) do
if type(goal) == "table" then
win_goals[field] = goal[1]
end
end
self:_advance_win_state(win, win_goals, time)
self:_advance_win_stage(win)
stage = self.win_stages[win]
win_goals = self:_get_win_goals(win, stage, open_windows)
end
if win_goals.time and not self.timers[win] then
self:_start_timer(win)
end
self:_advance_win_state(win, win_goals, time)
if self:_is_complete(win, win_goals) and not win_goals.time then
self:_advance_win_stage(win)
end
end
function WindowAnimator:_is_complete(win, goals)
local complete = true
local win_state = self.win_states[win]
if not win_state then
return true
end
for field, goal in pairs(goals) do
if field ~= "time" then
if type(goal) == "table" then
if goal.complete then
complete = goal.complete(win_state[field].position)
else
complete = goal[1] == round(win_state[field].position, 2)
end
end
if not complete then
break
end
end
end
return complete
end
function WindowAnimator:_advance_win_state(win, goals, time)
local win_state = self.win_states[win]
local win_configs = {}
local function win_conf(win_)
if win_configs[win_] then
return win_configs[win_]
end
local exists, conf = util.get_win_config(win_)
if not exists then
self:_remove_win(win_)
return
end
win_configs[win_] = conf
return conf
end
for field, goal in pairs(goals) do
if field ~= "time" then
local goal_type = type(goal)
-- Handle spring goal
if goal_type == "table" and goal[1] then
if not win_state[field] then
if field == "opacity" then
win_state[field] = { position = self.notif_bufs[win].highlights:get_opacity() }
else
local conf = win_conf(win)
if not conf then
return true
end
win_state[field] = { position = conf[field] }
end
end
animate.spring(time, goal[1], win_state[field], goal.frequency or 1, goal.damping or 1)
--- Directly move goal
elseif goal_type ~= "table" then
win_state[field] = { position = goal }
else
error("nvim-notify: Invalid stage goal: " .. vim.inspect(goal))
end
end
end
return self:_apply_win_state(win, win_state)
end
function WindowAnimator:_get_win_goals(win, win_stage, open_windows)
local notif_buf = self.notif_bufs[win]
local win_goals = self.stages[win_stage]({
buffer = notif_buf:buffer(),
message = self:_get_dimensions(notif_buf),
open_windows = open_windows,
}, win)
return win_goals
end
function WindowAnimator:_get_dimensions(notif_buf)
return {
height = math.min(self.config.max_height() or 1000, notif_buf:height()),
width = math.min(self.config.max_width() or 1000, notif_buf:width()),
}
end
function WindowAnimator:_apply_win_state(win, win_state)
local win_updated = false
if win_state.opacity then
win_updated = true
local notif_buf = self.notif_bufs[win]
if notif_buf:is_valid() then
notif_buf.highlights:set_opacity(win_state.opacity.position)
vim.fn.setwinvar(
win,
"&winhl",
"Normal:" .. notif_buf.highlights.body .. ",FloatBorder:" .. notif_buf.highlights.border
)
end
end
local exists, conf = util.get_win_config(win)
local new_conf = {}
if not exists then
self:_remove_win(win)
else
local function set_field(field, min, round_to)
if not win_state[field] then
return
end
local new_value = max(round(win_state[field].position, round_to), min)
if new_value == conf[field] then
return
end
win_updated = true
new_conf[field] = new_value
end
set_field("row", 0, 1)
set_field("col", 0, 1)
set_field("width", 1)
set_field("height", 1)
if win_updated then
if new_conf.row or new_conf.col then
new_conf.relative = conf.relative
new_conf.row = new_conf.row or conf.row
new_conf.col = new_conf.col or conf.col
end
api.nvim_win_set_config(win, new_conf)
end
end
return win_updated
end
---@return WindowAnimator
return function(stages, config)
return WindowAnimator:new(stages, config)
end

View File

@ -0,0 +1,118 @@
local pickers = require("telescope.pickers")
local finders = require("telescope.finders")
local conf = require("telescope.config").values
local actions = require("telescope.actions")
local action_state = require("telescope.actions.state")
local previewers = require("telescope.previewers")
local entry_display = require("telescope.pickers.entry_display")
local notify = require("notify")
local widths = {
time = 8,
title = nil,
icon = nil,
level = nil,
message = nil,
}
local displayer = entry_display.create({
separator = " ",
items = {
{ width = widths.time },
{ width = widths.title },
{ width = widths.icon },
{ width = widths.level },
{ width = widths.message },
},
})
local telescope_notifications = function(opts)
local time_format = require("notify")._config().time_formats().notification
local notifs = require("notify").history()
local reversed = {}
for i, notif in ipairs(notifs) do
reversed[#notifs - i + 1] = notif
end
pickers
.new(opts, {
results_title = "Notifications",
prompt_title = "Filter Notifications",
finder = finders.new_table({
results = reversed,
entry_maker = function(notif)
return {
value = notif,
display = function(entry)
return displayer({
{ vim.fn.strftime(time_format, entry.value.time), "NotifyLogTime" },
{ entry.value.title[1], "NotifyLogTitle" },
{ entry.value.icon, "Notify" .. entry.value.level .. "Title" },
{ entry.value.level, "Notify" .. entry.value.level .. "Title" },
{ entry.value.message[1], "Notify" .. entry.value.level .. "Body" },
})
end,
ordinal = notif.title[1] .. " " .. notif.title[2] .. " " .. table.concat(
notif.message,
" "
),
}
end,
}),
sorter = conf.generic_sorter(opts),
attach_mappings = function(prompt_bufnr, map)
actions.select_default:replace(function()
actions.close(prompt_bufnr)
local selection = action_state.get_selected_entry()
if selection == nil then
return
end
local notification = selection.value
local opened_buffer = notify.open(notification)
local lines = vim.opt.lines:get()
local cols = vim.opt.columns:get()
local win = vim.api.nvim_open_win(opened_buffer.buffer, true, {
relative = "editor",
row = (lines - opened_buffer.height) / 2,
col = (cols - opened_buffer.width) / 2,
height = opened_buffer.height,
width = opened_buffer.width,
border = "rounded",
style = "minimal",
})
-- vim.wo does not behave like setlocal, thus we use setwinvar to set local
-- only options. Otherwise our changes would affect subsequently opened
-- windows.
-- see e.g. neovim#14595
vim.fn.setwinvar(
win,
"&winhl",
"Normal:"
.. opened_buffer.highlights.body
.. ",FloatBorder:"
.. opened_buffer.highlights.border
)
vim.fn.setwinvar(win, "&wrap", 0)
end)
return true
end,
previewer = previewers.new_buffer_previewer({
title = "Message",
define_preview = function(self, entry, status)
local notification = entry.value
local max_width = vim.api.nvim_win_get_config(status.preview_win).width
vim.api.nvim_win_set_option(status.preview_win, "wrap", true)
notify.open(notification, { buffer = self.state.bufnr, max_width = max_width })
end,
}),
})
:find()
end
return require("telescope").register_extension({
exports = {
notify = telescope_notifications,
},
})

View File

@ -0,0 +1,3 @@
#!/nix/store/306znyj77fv49kwnkpxmb0j2znqpa8bj-bash-5.2p26/bin/bash
nvim --headless -c "luafile ./scripts/gendocs.lua" -c 'qa'

View File

@ -0,0 +1,804 @@
-- TODO: A lot of this is private code from minidoc, which could be removed if made public
local minidoc = require("mini.doc")
local H = {}
--stylua: ignore start
H.pattern_sets = {
-- Patterns for working with afterlines. At the moment deliberately crafted
-- to work only on first line without indent.
-- Determine if line is a function definition. Captures function name and
-- arguments. For reference see '2.5.9 Function Definitions' in Lua manual.
afterline_fundef = {
'^function%s+(%S-)(%b())', -- Regular definition
'^local%s+function%s+(%S-)(%b())', -- Local definition
'^(%S+)%s*=%s*function(%b())', -- Regular assignment
'^local%s+(%S+)%s*=%s*function(%b())', -- Local assignment
},
-- Determine if line is a general assignment
afterline_assign = {
'^(%S-)%s*=', -- General assignment
'^local%s+(%S-)%s*=', -- Local assignment
},
-- Patterns to work with type descriptions
-- (see https://github.com/sumneko/lua-language-server/wiki/EmmyLua-Annotations#types-and-type)
types = {
'table%b<>',
'fun%b(): %S+', 'fun%b()', 'async fun%b(): %S+', 'async fun%b()',
'nil', 'any', 'boolean', 'string', 'number', 'integer', 'function', 'table', 'thread', 'userdata', 'lightuserdata',
'%.%.%.',
"%S+",
},
}
H.apply_config = function(config)
MiniDoc.config = config
end
H.is_disabled = function()
return vim.g.minidoc_disable == true or vim.b.minidoc_disable == true
end
H.get_config = function(config)
return vim.tbl_deep_extend("force", MiniDoc.config, vim.b.minidoc_config or {}, config or {})
end
-- Work with project specific script ==========================================
H.execute_project_script = function(input, output, config)
-- Don't process script if there are more than one active `generate` calls
if H.generate_is_active then
return
end
-- Don't process script if at least one argument is not default
if not (input == nil and output == nil and config == nil) then
return
end
-- Store information
local global_config_cache = vim.deepcopy(MiniDoc.config)
local local_config_cache = vim.b.minidoc_config
-- Pass information to a possible `generate()` call inside script
H.generate_is_active = true
H.generate_recent_output = nil
-- Execute script
local success = pcall(vim.cmd, "luafile " .. H.get_config(config).script_path)
-- Restore information
MiniDoc.config = global_config_cache
vim.b.minidoc_config = local_config_cache
H.generate_is_active = nil
return success
end
-- Default documentation targets ----------------------------------------------
H.default_input = function()
-- Search in current and recursively in other directories for files with
-- 'lua' extension
local res = {}
for _, dir_glob in ipairs({ ".", "lua/**", "after/**", "colors/**" }) do
local files = vim.fn.globpath(dir_glob, "*.lua", false, true)
-- Use full paths
files = vim.tbl_map(function(x)
return vim.fn.fnamemodify(x, ":p")
end, files)
-- Put 'init.lua' first among files from same directory
table.sort(files, function(a, b)
if vim.fn.fnamemodify(a, ":h") == vim.fn.fnamemodify(b, ":h") then
if vim.fn.fnamemodify(a, ":t") == "init.lua" then
return true
end
if vim.fn.fnamemodify(b, ":t") == "init.lua" then
return false
end
end
return a < b
end)
table.insert(res, files)
end
return vim.tbl_flatten(res)
end
H.default_output = function()
local cur_dir = vim.fn.fnamemodify(vim.loop.cwd(), ":t:r")
return ("doc/%s.txt"):format(cur_dir)
end
-- Parsing --------------------------------------------------------------------
H.lines_to_block_arr = function(lines, config)
local matched_prev, matched_cur
local res = {}
local block_raw = { annotation = {}, section_id = {}, afterlines = {}, line_begin = 1 }
for i, l in ipairs(lines) do
local from, to, section_id = config.annotation_extractor(l)
matched_prev, matched_cur = matched_cur, from ~= nil
if matched_cur then
if not matched_prev then
-- Finish current block
block_raw.line_end = i - 1
table.insert(res, H.raw_block_to_block(block_raw, config))
-- Start new block
block_raw = { annotation = {}, section_id = {}, afterlines = {}, line_begin = i }
end
-- Add annotation line without matched annotation pattern
table.insert(block_raw.annotation, ("%s%s"):format(l:sub(0, from - 1), l:sub(to + 1)))
-- Add section id (it is empty string in case of no section id capture)
table.insert(block_raw.section_id, section_id or "")
else
-- Add afterline
table.insert(block_raw.afterlines, l)
end
end
block_raw.line_end = #lines
table.insert(res, H.raw_block_to_block(block_raw, config))
return res
end
-- Raw block structure is an intermediate step added for convenience. It is
-- a table with the following keys:
-- - `annotation` - lines (after removing matched annotation pattern) that were
-- parsed as annotation.
-- - `section_id` - array with length equal to `annotation` length with strings
-- captured as section id. Empty string of no section id was captured.
-- - Everything else is used as block info (like `afterlines`, etc.).
H.raw_block_to_block = function(block_raw, config)
if #block_raw.annotation == 0 and #block_raw.afterlines == 0 then
return nil
end
local block = H.new_struct("block", {
afterlines = block_raw.afterlines,
line_begin = block_raw.line_begin,
line_end = block_raw.line_end,
})
local block_begin = block.info.line_begin
-- Parse raw block annotation lines from top to bottom. New section starts
-- when section id is detected in that line.
local section_cur = H.new_struct(
"section",
{ id = config.default_section_id, line_begin = block_begin }
)
for i, annotation_line in ipairs(block_raw.annotation) do
local id = block_raw.section_id[i]
if id ~= "" then
-- Finish current section
if #section_cur > 0 then
section_cur.info.line_end = block_begin + i - 2
block:insert(section_cur)
end
-- Start new section
section_cur = H.new_struct("section", { id = id, line_begin = block_begin + i - 1 })
end
section_cur:insert(annotation_line)
end
if #section_cur > 0 then
section_cur.info.line_end = block_begin + #block_raw.annotation - 1
block:insert(section_cur)
end
return block
end
-- Hooks ----------------------------------------------------------------------
H.apply_structure_hooks = function(doc, hooks)
for _, file in ipairs(doc) do
for _, block in ipairs(file) do
hooks.block_pre(block)
for _, section in ipairs(block) do
hooks.section_pre(section)
local hook = hooks.sections[section.info.id]
if hook ~= nil then
hook(section)
end
hooks.section_post(section)
end
hooks.block_post(block)
end
hooks.file(file)
end
hooks.doc(doc)
end
H.alias_register = function(s)
if #s == 0 then
return
end
-- Remove first word (with bits of surrounding whitespace) while capturing it
local alias_name
s[1] = s[1]:gsub("%s*(%S+) ?", function(x)
alias_name = x
return ""
end, 1)
if alias_name == nil then
return
end
MiniDoc.current.aliases = MiniDoc.current.aliases or {}
MiniDoc.current.aliases[alias_name] = table.concat(s, "\n")
end
H.alias_replace = function(s)
if MiniDoc.current.aliases == nil then
return
end
for i, _ in ipairs(s) do
for alias_name, alias_desc in pairs(MiniDoc.current.aliases) do
-- Escape special characters. This is done here and not while registering
-- alias to allow user to refer to aliases by its original name.
-- Store escaped words in separate variables because `vim.pesc()` returns
-- two values which might conflict if outputs are used as arguments.
local name_escaped = vim.pesc(alias_name)
local desc_escaped = vim.pesc(alias_desc)
s[i] = s[i]:gsub(name_escaped, desc_escaped)
end
end
end
H.toc_register = function(s)
MiniDoc.current.toc = MiniDoc.current.toc or {}
table.insert(MiniDoc.current.toc, s)
end
H.toc_insert = function(s)
if MiniDoc.current.toc == nil then
return
end
-- Render table of contents
local toc_lines = {}
for _, toc_entry in ipairs(MiniDoc.current.toc) do
local _, tag_section = toc_entry.parent:has_descendant(function(x)
return type(x) == "table" and x.type == "section" and x.info.id == "@tag"
end)
tag_section = tag_section or {}
local lines = {}
for i = 1, math.max(#toc_entry, #tag_section) do
local left = toc_entry[i] or ""
-- Use tag refernce instead of tag enclosure
local right = string.match(tag_section[i], "%*.*%*"):gsub("%*", "|")
-- local right = vim.trim((tag_section[i] or ""):gsub("%*", "|"))
-- Add visual line only at first entry (while not adding trailing space)
local filler = i == 1 and "." or (right == "" and "" or " ")
-- Make padding of 2 spaces at both left and right
local n_filler = math.max(74 - H.visual_text_width(left) - H.visual_text_width(right), 3)
table.insert(lines, (" %s%s%s"):format(left, filler:rep(n_filler), right))
end
table.insert(toc_lines, lines)
-- Don't show `toc_entry` lines in output
toc_entry:clear_lines()
end
for _, l in ipairs(vim.tbl_flatten(toc_lines)) do
s:insert(l)
end
end
H.add_section_heading = function(s, heading)
if #s == 0 or s.type ~= "section" then
return
end
-- Add heading
s:insert(1, ("%s~"):format(heading))
end
H.enclose_var_name = function(s)
if #s == 0 or s.type ~= "section" then
return
end
s[1] = s[1]:gsub("(%S+)", "{%1}", 1)
end
---@param init number Start of searching for first "type-like" string. It is
--- needed to not detect type early. Like in `@param a_function function`.
---@private
H.enclose_type = function(s, enclosure, init)
if #s == 0 or s.type ~= "section" then
return
end
enclosure = enclosure or "`%(%1%)`"
init = init or 1
local cur_type = H.match_first_pattern(s[1], H.pattern_sets["types"], init)
if #cur_type == 0 then
return
end
-- Add `%S*` to front and back of found pattern to support their combination
-- with `|`. Also allows using `[]` and `?` prefixes.
local type_pattern = ("(%%S*%s%%S*)"):format(vim.pesc(cur_type[1]))
-- Avoid replacing possible match before `init`
local l_start = s[1]:sub(1, init - 1)
local l_end = s[1]:sub(init):gsub(type_pattern, enclosure, 1)
s[1] = ("%s%s"):format(l_start, l_end)
end
-- Infer data from afterlines -------------------------------------------------
H.infer_header = function(b)
local has_signature = b:has_descendant(function(x)
return type(x) == "table" and x.type == "section" and x.info.id == "@signature"
end)
local has_tag = b:has_descendant(function(x)
return type(x) == "table" and x.type == "section" and x.info.id == "@tag"
end)
if has_signature and has_tag then
return
end
local l_all = table.concat(b.info.afterlines, " ")
local tag, signature
-- Try function definition
local fun_capture = H.match_first_pattern(l_all, H.pattern_sets["afterline_fundef"])
if #fun_capture > 0 then
tag = tag or ("%s()"):format(fun_capture[1])
signature = signature or ("%s%s"):format(fun_capture[1], fun_capture[2])
end
-- Try general assignment
local assign_capture = H.match_first_pattern(l_all, H.pattern_sets["afterline_assign"])
if #assign_capture > 0 then
tag = tag or assign_capture[1]
signature = signature or assign_capture[1]
end
if tag ~= nil then
-- First insert signature (so that it will appear after tag section)
if not has_signature then
b:insert(1, H.as_struct({ signature }, "section", { id = "@signature" }))
end
-- Insert tag
if not has_tag then
b:insert(1, H.as_struct({ tag }, "section", { id = "@tag" }))
end
end
end
function H.is_module(name)
if string.find(name, "%(") then
return false
end
if string.find(name, "[A-Z]") then
return false
end
return true
end
H.format_signature = function(line)
-- Try capture function signature
local name, args = line:match("(%S-)(%b())")
-- Otherwise pick first word
name = name or line:match("(%S+)")
if not args and H.is_module(name) then
return ""
end
local name_elems = vim.split(name, ".", { plain = true })
name = name_elems[#name_elems]
if not name then
return ""
end
-- Tidy arguments
if args and args ~= "()" then
local arg_parts = vim.split(args:sub(2, -2), ",")
local arg_list = {}
for _, a in ipairs(arg_parts) do
-- Enclose argument in `{}` while controlling whitespace
table.insert(arg_list, ("{%s}"):format(vim.trim(a)))
end
args = ("(%s)"):format(table.concat(arg_list, ", "))
end
return ("`%s`%s"):format(name, args or "")
end
-- Work with structures -------------------------------------------------------
-- Constructor
H.new_struct = function(struct_type, info)
local output = {
info = info or {},
type = struct_type,
}
output.insert = function(self, index, child)
-- Allow both `x:insert(child)` and `x:insert(1, child)`
if child == nil then
child, index = index, #self + 1
end
if type(child) == "table" then
child.parent = self
child.parent_index = index
end
table.insert(self, index, child)
H.sync_parent_index(self)
end
output.remove = function(self, index)
index = index or #self
table.remove(self, index)
H.sync_parent_index(self)
end
output.has_descendant = function(self, predicate)
local bool_res, descendant = false, nil
H.apply_recursively(function(x)
if not bool_res and predicate(x) then
bool_res = true
descendant = x
end
end, self)
return bool_res, descendant
end
output.has_lines = function(self)
return self:has_descendant(function(x)
return type(x) == "string"
end)
end
output.clear_lines = function(self)
for i, x in ipairs(self) do
if type(x) == "string" then
self[i] = nil
else
x:clear_lines()
end
end
end
return output
end
H.sync_parent_index = function(x)
for i, _ in ipairs(x) do
if type(x[i]) == "table" then
x[i].parent_index = i
end
end
return x
end
-- Converter (this ensures that children have proper parent-related data)
H.as_struct = function(array, struct_type, info)
-- Make default info `info` for cases when structure is created manually
local default_info = ({
section = { id = "@text", line_begin = -1, line_end = -1 },
block = { afterlines = {}, line_begin = -1, line_end = -1 },
file = { path = "" },
doc = { input = {}, output = "", config = H.get_config() },
})[struct_type]
info = vim.tbl_deep_extend("force", default_info, info or {})
local res = H.new_struct(struct_type, info)
for _, x in ipairs(array) do
res:insert(x)
end
return res
end
-- Work with text -------------------------------------------------------------
H.ensure_indent = function(text, n_indent_target)
local lines = vim.split(text, "\n")
local n_indent, n_indent_cur = math.huge, math.huge
-- Find number of characters in indent
for _, l in ipairs(lines) do
-- Update lines indent: minimum of all indents except empty lines
if n_indent > 0 then
_, n_indent_cur = l:find("^%s*")
-- Condition "current n-indent equals line length" detects empty line
if (n_indent_cur < n_indent) and (n_indent_cur < l:len()) then
n_indent = n_indent_cur
end
end
end
-- Ensure indent
local indent = string.rep(" ", n_indent_target)
for i, l in ipairs(lines) do
if l ~= "" then
lines[i] = indent .. l:sub(n_indent + 1)
end
end
return table.concat(lines, "\n")
end
H.align_text = function(text, width, direction)
if type(text) ~= "string" then
return
end
text = vim.trim(text)
width = width or 78
direction = direction or "left"
-- Don't do anything if aligning left or line is a whitespace
if direction == "left" or text:find("^%s*$") then
return text
end
local n_left = math.max(0, 78 - H.visual_text_width(text))
if direction == "center" then
n_left = math.floor(0.5 * n_left)
end
return (" "):rep(n_left) .. text
end
H.visual_text_width = function(text)
-- Ignore concealed characters (usually "invisible" in 'help' filetype)
local _, n_concealed_chars = text:gsub("([*|`])", "%1")
return vim.fn.strdisplaywidth(text) - n_concealed_chars
end
--- Return earliest match among many patterns
---
--- Logic here is to test among several patterns. If several got a match,
--- return one with earliest match.
---
---@private
H.match_first_pattern = function(text, pattern_set, init)
local start_tbl = vim.tbl_map(function(pattern)
return text:find(pattern, init) or math.huge
end, pattern_set)
local min_start, min_id = math.huge, nil
for id, st in ipairs(start_tbl) do
if st < min_start then
min_start, min_id = st, id
end
end
if min_id == nil then
return {}
end
return { text:match(pattern_set[min_id], init) }
end
-- Utilities ------------------------------------------------------------------
H.apply_recursively = function(f, x, used)
used = used or {}
if used[x] then
return
end
f(x)
used[x] = true
if type(x) == "table" then
for _, t in ipairs(x) do
H.apply_recursively(f, t, used)
end
end
end
H.collect_strings = function(x)
local res = {}
H.apply_recursively(function(y)
if type(y) == "string" then
-- Allow `\n` in strings
table.insert(res, vim.split(y, "\n"))
end
end, x)
-- Flatten to only have strings and not table of strings (from `vim.split`)
return vim.tbl_flatten(res)
end
H.file_read = function(path)
local file = assert(io.open(path))
local contents = file:read("*all")
file:close()
return vim.split(contents, "\n")
end
H.file_write = function(path, lines)
-- Ensure target directory exists
local dir = vim.fn.fnamemodify(path, ":h")
vim.fn.mkdir(dir, "p")
-- Write to file
vim.fn.writefile(lines, path, "b")
end
H.full_path = function(path)
return vim.fn.resolve(vim.fn.fnamemodify(path, ":p"))
end
H.message = function(msg)
vim.cmd("echomsg " .. vim.inspect("(mini.doc) " .. msg))
end
minidoc.setup({})
minidoc.generate(
{
"./lua/notify/init.lua",
"./lua/notify/config/init.lua",
"./lua/notify/render/init.lua",
},
nil,
{
hooks = vim.tbl_extend("force", minidoc.default_hooks, {
block_post = function(b)
if not b:has_lines() then return end
local found_param, found_field = false, false
local n_tag_sections = 0
H.apply_recursively(function(x)
if not (type(x) == 'table' and x.type == 'section') then return end
-- Add headings before first occurence of a section which type usually
-- appear several times
if not found_param and x.info.id == '@param' then
H.add_section_heading(x, 'Parameters')
found_param = true
end
if not found_field and x.info.id == '@field' then
H.add_section_heading(x, 'Fields')
found_field = true
end
if x.info.id == '@tag' then
local text = x[1]
local tag = string.match(text, "%*.*%*")
local prefix = (string.sub(tag, 2, #tag - 1))
if not H.is_module(prefix) then
prefix = ""
end
local n_filler = math.max(78 - H.visual_text_width(prefix) - H.visual_text_width(tag), 3)
local line = ("%s%s%s"):format(prefix, (" "):rep(n_filler), tag)
x:remove(1)
x:insert(1, line)
x.parent:remove(x.parent_index)
n_tag_sections = n_tag_sections + 1
x.parent:insert(n_tag_sections, x)
end
end, b)
-- b:insert(1, H.as_struct({ string.rep('=', 78) }, 'section'))
b:insert(H.as_struct({ '' }, 'section'))
end,
doc = function(d)
-- Render table of contents
H.apply_recursively(function(x)
if not (type(x) == 'table' and x.type == 'section' and x.info.id == '@toc') then return end
H.toc_insert(x)
end, d)
-- Insert modeline
d:insert(
H.as_struct(
{ H.as_struct({ H.as_struct({ ' vim:tw=78:ts=8:noet:ft=help:norl:' }, 'section') }, 'block') },
'file'
)
)
end,
sections = {
['@generic'] = function(s)
s:remove(1)
end,
['@field'] = function(s)
-- H.mark_optional(s)
if string.find(s[1], "^private ") then
s:remove(1)
return
end
H.enclose_var_name(s)
H.enclose_type(s, '`%(%1%)`', s[1]:find('%s'))
end,
['@alias'] = function(s)
local name = s[1]:match('%s*(%S*)')
local alias = s[1]:match('%s(.*)$')
s[1] = ("`%s` → `%s`"):format(name, alias)
H.add_section_heading(s, 'Alias')
s:insert(1, H.as_struct({ ("*%s*"):format(name) }, "section", { id = "@tag" }))
end,
['@param'] = function(s)
H.enclose_var_name(s)
H.enclose_type(s, '`%(%1%)`', s[1]:find('%s'))
end,
['@return'] = function(s)
H.enclose_type(s, '`%(%1%)`', 1)
H.add_section_heading(s, 'Return')
end,
['@nodoc'] = function(s) s.parent:clear_lines() end,
['@class'] = function(s)
H.enclose_var_name(s)
-- Add heading
local line = s[1]
s:remove(1)
local class_name = string.match(line, "%{(.*)%}")
local inherits = string.match(line, ": (.*)")
if inherits then
s:insert(1, ("Inherits: `%s`"):format(inherits))
s:insert(2, "")
end
s:insert(1, H.as_struct({ ("*%s*"):format(class_name) }, "section", { id = "@tag" }))
end,
['@signature'] = function(s)
s[1] = H.format_signature(s[1])
if s[1] ~= "" then
table.insert(s, "")
end
end,
},
file = function(f)
if not f:has_lines() then
return
end
if f.info.path ~= "./lua/notify/init.lua" then
f:insert(1, H.as_struct({ H.as_struct({ string.rep("=", 78) }, "section") }, "block"))
f:insert(H.as_struct({ H.as_struct({ "" }, "section") }, "block"))
else
f:insert(
1,
H.as_struct(
{
H.as_struct(
{ "*nvim-notify.txt* A fancy, configurable notification manager for NeoVim" },
"section"
),
},
"block"
)
)
f:insert(2, H.as_struct({ H.as_struct({ "" }, "section") }, "block"))
f:insert(3, H.as_struct({ H.as_struct({ string.rep("=", 78) }, "section") }, "block"))
f:insert(H.as_struct({ H.as_struct({ "" }, "section") }, "block"))
end
end,
}),
}
)

View File

@ -0,0 +1,3 @@
#!/nix/store/306znyj77fv49kwnkpxmb0j2znqpa8bj-bash-5.2p26/bin/bash
stylua lua tests

View File

@ -0,0 +1,20 @@
#!/nix/store/306znyj77fv49kwnkpxmb0j2znqpa8bj-bash-5.2p26/bin/bash
tempfile=".test_output.tmp"
if [[ -n $1 ]]; then
nvim --headless --noplugin -u tests/init.vim -c "PlenaryBustedFile $1" | tee "${tempfile}"
else
nvim --headless --noplugin -u tests/init.vim -c "PlenaryBustedDirectory tests/ {minimal_init = 'tests/init.vim'}" | tee "${tempfile}"
fi
# Plenary doesn't emit exit code 1 when tests have errors during setup
errors=$(sed 's/\x1b\[[0-9;]*m//g' "${tempfile}" | awk '/(Errors|Failed) :/ {print $3}' | grep -v '0')
rm "${tempfile}"
if [[ -n $errors ]]; then
echo "Tests failed"
exit 1
fi
exit 0

View File

@ -0,0 +1,5 @@
column_width = 100
line_endings = "Unix"
indent_type = "Spaces"
indent_width = 2
quote_style = "AutoPreferDouble"

View File

@ -0,0 +1,4 @@
set rtp+=.
set rtp+=../plenary.nvim
set termguicolors
runtime! plugin/plenary.vim

View File

@ -0,0 +1,160 @@
local async = require("plenary.async")
async.tests.add_to_env()
vim.opt.termguicolors = true
A = vim.schedule_wrap(function(...)
print(vim.inspect(...))
end)
describe("checking public interface", function()
local notify = require("notify")
local async_notify = require("notify").async
assert:add_formatter(vim.inspect)
before_each(function()
notify.setup({ background_colour = "#000000" })
notify.dismiss({ pending = true, silent = true })
end)
describe("notifications", function()
it("returns all previous notifications", function()
notify.notify("test", "error")
local notifs = notify.history()
assert.are.same({
{
icon = "",
id = 1,
level = "ERROR",
message = { "test" },
render = notifs[1].render,
time = notifs[1].time,
title = { "", notifs[1].title[2] },
},
}, notifs)
end)
describe("rendering", function()
a.it("uses custom render in config", function()
local called = false
notify.setup({
background_colour = "#000000",
render = function()
called = true
end,
})
notify.async("test", "error").events.open()
assert.is.True(called)
end)
a.it("uses custom render in call", function()
local called = false
notify
.async("test", "error", {
render = function()
called = true
end,
}).events
.open()
assert.is.True(called)
end)
end)
describe("replacing", function()
it("inherits options", function()
local orig = notify.notify("first", "info", { title = "test", icon = "x" })
local next = notify.notify("second", nil, { replace = orig })
assert.are.same(
next,
vim.tbl_extend("force", orig, { id = next.id, message = next.message })
)
end)
a.it("uses same window", function()
local orig = async_notify("first", "info", { timeout = false })
local win = orig.events.open()
async_notify("second", nil, { replace = orig, timeout = 100 })
async.util.scheduler()
local found = false
local bufs = vim.api.nvim_list_bufs()
for _, buf in ipairs(bufs) do
if vim.api.nvim_buf_get_lines(buf, 0, -1, false)[1] == "second" then
assert.Not(found)
assert.same(vim.fn.bufwinid(buf), win)
found = true
end
end
end)
end)
end)
a.it("uses the confgured minimum width", function()
notify.setup({
background_colour = "#000000",
minimum_width = 20,
})
local win = notify.async("test").events.open()
assert.equal(vim.api.nvim_win_get_width(win), 20)
end)
a.it("uses the configured max width", function()
notify.setup({
background_colour = "#000000",
max_width = function()
return 3
end,
})
local win = notify.async("test").events.open()
assert.equal(vim.api.nvim_win_get_width(win), 3)
end)
a.it("uses the configured max height", function()
local instance = notify.instance({
background_colour = "#000000",
max_height = function()
return 3
end,
}, false)
local win = instance.async("test").events.open()
assert.equal(vim.api.nvim_win_get_height(win), 3)
end)
a.it("renders title as longest line", function()
local instance = notify.instance({
background_colour = "#000000",
minimum_width = 10,
}, false)
local win = instance.async("test", nil, { title = { string.rep("a", 16), "" } }).events.open()
assert.equal(21, vim.api.nvim_win_get_width(win))
end)
a.it("renders notification above config level", function()
local win =
notify.async("test", "info", { message = { string.rep("a", 16), "" } }).events.open()
assert.Not.Nil(vim.api.nvim_win_get_config(win))
end)
a.it("doesn't render notification below config level", function()
async.run(function()
local notif = notify.async("test", "debug", { message = { string.rep("a", 16), "" } })
local win = notif.events.open()
async.api.nvim_buf_set_option(async.api.nvim_win_get_buf(win), "filetype", "test")
end)
async.util.sleep(100)
local bufs = vim.api.nvim_list_bufs()
for _, buf in ipairs(bufs) do
assert.Not.same(vim.api.nvim_buf_get_option(buf, "filetype"), "test")
end
end)
a.it("refreshes timeout on replace", function()
-- Don't want to spend time animating
notify.setup({ background_colour = "#000000", stages = "static" })
local notif = notify.async("test", "error", { timeout = 500 })
local win = notif.events.open()
a.util.sleep(300)
notify.async("test2", "error", { replace = notif })
a.util.sleep(300)
a.util.scheduler()
assert(vim.api.nvim_win_is_valid(win))
end)
end)