The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .github
    ├── FUNDING.yml
    └── workflows
    │   ├── docgen.yaml
    │   └── workflow.yaml
├── .gitignore
├── .releaserc.json
├── Dockerfile
├── LICENSE
├── README.md
├── doc
    └── nvim-notify.txt
├── lua
    ├── notify
    │   ├── animate
    │   │   ├── init.lua
    │   │   └── spring.lua
    │   ├── config
    │   │   ├── highlights.lua
    │   │   └── init.lua
    │   ├── init.lua
    │   ├── instance.lua
    │   ├── integrations
    │   │   ├── fzf.lua
    │   │   └── init.lua
    │   ├── render
    │   │   ├── base.lua
    │   │   ├── compact.lua
    │   │   ├── default.lua
    │   │   ├── init.lua
    │   │   ├── minimal.lua
    │   │   ├── simple.lua
    │   │   ├── wrapped-compact.lua
    │   │   └── wrapped-default.lua
    │   ├── service
    │   │   ├── buffer
    │   │   │   ├── highlights.lua
    │   │   │   └── init.lua
    │   │   ├── init.lua
    │   │   └── notification.lua
    │   ├── stages
    │   │   ├── fade.lua
    │   │   ├── fade_in_slide_out.lua
    │   │   ├── init.lua
    │   │   ├── no_animation.lua
    │   │   ├── slide.lua
    │   │   ├── slide_out.lua
    │   │   ├── static.lua
    │   │   └── util.lua
    │   ├── util
    │   │   ├── init.lua
    │   │   └── queue.lua
    │   └── windows
    │   │   └── init.lua
    └── telescope
    │   └── _extensions
    │       └── notify.lua
├── scripts
    ├── docgen
    ├── gendocs.lua
    ├── style
    └── test
├── stylua.toml
└── tests
    ├── init.vim
    ├── manual
        └── merge_duplicates.lua
    └── unit
        └── init_spec.lua


/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: rcarriga
2 | 


--------------------------------------------------------------------------------
/.github/workflows/docgen.yaml:
--------------------------------------------------------------------------------
 1 | # Taken from telescope
 2 | name: Generate docs
 3 | 
 4 | on:
 5 |   push:
 6 |     branches-ignore:
 7 |       - master
 8 | 
 9 | jobs:
10 |   build-sources:
11 |     name: Generate docs
12 |     runs-on: ${{ matrix.os }}
13 |     strategy:
14 |       fail-fast: false
15 |       matrix:
16 |         include:
17 |           - os: ubuntu-20.04
18 |             url: https://github.com/neovim/neovim/releases/download/v0.5.1/nvim-linux64.tar.gz
19 |     steps:
20 |       - uses: actions/checkout@v2
21 |       - run: date +%F > todays-date
22 |       - name: Restore cache for today's nightly.
23 |         uses: actions/cache@v2
24 |         with:
25 |           path: _neovim
26 |           key: ${{ runner.os }}-${{ matrix.url }}-${{ hashFiles('todays-date') }}
27 | 
28 |       - name: Prepare
29 |         run: |
30 |           test -d _neovim || {
31 |             mkdir -p _neovim
32 |             curl -sL ${{ matrix.url }} | tar xzf - --strip-components=1 -C "${PWD}/_neovim"
33 |           }
34 |           mkdir -p ~/.local/share/nvim/site/pack/vendor/start
35 |           git clone --depth 1 https://github.com/nvim-lua/plenary.nvim ~/.local/share/nvim/site/pack/vendor/start/plenary.nvim
36 |           git clone --depth 1 https://github.com/tjdevries/tree-sitter-lua ~/.local/share/nvim/site/pack/vendor/start/tree-sitter-lua
37 |           ln -s $(pwd) ~/.local/share/nvim/site/pack/vendor/start
38 |       - name: Build parser
39 |         run: |
40 |           # We have to build the parser every single time to keep up with parser changes
41 |           cd ~/.local/share/nvim/site/pack/vendor/start/tree-sitter-lua
42 |           make dist
43 |           cd -
44 |       - name: Generating docs
45 |         run: |
46 |           export PATH="${PWD}/_neovim/bin:${PATH}"
47 |           export VIM="${PWD}/_neovim/share/nvim/runtime"
48 |           nvim --version
49 |           ./scripts/docgen
50 |       - name: Update documentation
51 |         env:
52 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
53 |           COMMIT_MSG: |
54 |             docs: update doc/nvim-notify.txt
55 |             skip-checks: true
56 |         run: |
57 |           git config user.email "actions@github"
58 |           git config user.name "Github Actions"
59 |           git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git
60 |           git add doc/
61 |           # Only commit and push if we have changes
62 |           git diff --quiet && git diff --staged --quiet || (git commit -m "${COMMIT_MSG}"; git push origin HEAD:${GITHUB_REF})
63 | 


--------------------------------------------------------------------------------
/.github/workflows/workflow.yaml:
--------------------------------------------------------------------------------
 1 | name: nvim-notify Workflow
 2 | on: 
 3 |   - push
 4 | jobs:
 5 |   style:
 6 |     name: style
 7 |     runs-on: ubuntu-20.04
 8 |     steps:
 9 |       - uses: actions/checkout@v2
10 |       - uses: JohnnyMorganz/stylua-action@v1
11 |         with:
12 |           token: ${{ secrets.GITHUB_TOKEN }}
13 |           version: latest
14 |           args: --check lua/ tests/
15 | 
16 |   tests:
17 |     name: tests
18 |     runs-on: ubuntu-20.04
19 |     strategy:
20 |       fail-fast: false
21 |       matrix:
22 |         include:
23 |           - os: ubuntu-20.04
24 |             nvim-version: '0.10.3'
25 |     steps:
26 |       - uses: actions/checkout@v2
27 |       - id: nvim-sha
28 |         run: |
29 |           curl -sL https://github.com/neovim/neovim/releases/download/v${{ matrix.nvim-version }}/nvim-linux64.tar.gz.sha256sum > nvim-sha
30 |           echo "cache-key=$(awk '{print $2 "-" $1}' nvim-sha)" >> "$GITHUB_OUTPUT"
31 |       - name: Restore cache for today's nightly.
32 |         uses: actions/cache@v2
33 |         with:
34 |           path: _neovim
35 |           key: ${{ steps.nvim-sha.outputs.cache-key }}
36 | 
37 |       - name: Prepare dependencies
38 |         run: |
39 |           test -d _neovim || {
40 |             mkdir -p _neovim
41 |             curl -sL https://github.com/neovim/neovim/releases/download/v${{ matrix.nvim-version }}/nvim-linux64.tar.gz | tar xzf - --strip-components=1 -C "${PWD}/_neovim"
42 |           }
43 |           mkdir -p ~/.local/share/nvim/site/pack/vendor/start
44 |           git clone --depth 1 https://github.com/nvim-lua/plenary.nvim ~/.local/share/nvim/site/pack/vendor/start/plenary.nvim
45 |           ln -s $(pwd) ~/.local/share/nvim/site/pack/vendor/start
46 | 
47 |       - name: Run tests
48 |         run: |
49 |           export PATH="${PWD}/_neovim/bin:${PATH}"
50 |           export VIM="${PWD}/_neovim/share/nvim/runtime"
51 |           nvim --version
52 |           ./scripts/test
53 |           
54 |   release:
55 |     name: release
56 |     if: ${{ github.ref == 'refs/heads/master' }}
57 |     needs: 
58 |       - style
59 |       - tests
60 |     runs-on: ubuntu-22.04
61 |     steps:
62 |       - name: Checkout
63 |         uses: actions/checkout@v2
64 |         with:
65 |           fetch-depth: 0
66 |       - name: Setup Node.js
67 |         uses: actions/setup-node@v1
68 |         with:
69 |           node-version: 20
70 |       - name: Release
71 |         env:
72 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
73 |         run: npx semantic-release
74 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | neovim/
2 | plenary.nvim/
3 | doc/tags
4 | 


--------------------------------------------------------------------------------
/.releaserc.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "plugins": [
 3 |     "@semantic-release/commit-analyzer",
 4 |     "@semantic-release/release-notes-generator",
 5 |     [
 6 |       "@semantic-release/github",
 7 |       {
 8 |         "successComment": false
 9 |       }
10 |     ]
11 |   ]
12 | }
13 | 


--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
 1 | 
 2 | ARG NEOVIM_RELEASE=${NEOVIM_RELEASE:-https://github.com/neovim/neovim/releases/download/nightly/nvim-linux64.tar.gz}
 3 | FROM ubuntu
 4 | ARG NEOVIM_RELEASE
 5 | 
 6 | RUN apt-get update
 7 | RUN apt-get -y install git curl tar gcc g++
 8 | RUN mkdir /neovim
 9 | RUN curl -sL ${NEOVIM_RELEASE} | tar xzf - --strip-components=1 -C "/neovim"
10 | RUN git clone --depth 1 https://github.com/nvim-lua/plenary.nvim
11 | RUN git clone --depth 1 https://github.com/tjdevries/tree-sitter-lua
12 | 
13 | WORKDIR tree-sitter-lua
14 | RUN mkdir -p build parser; \
15 |     cc -o ./build/parser.so -I ./src src/parser.c src/scanner.c -shared -Os -lstdc++ -fPIC; \
16 |     ln -s ../build/parser.so parser/lua.so;
17 | 
18 | RUN mkdir /notify
19 | WORKDIR /notify
20 | 
21 | 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'"]
22 | 


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


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
  1 | # nvim-notify
  2 | 
  3 | A fancy, configurable, notification manager for NeoVim
  4 | 
  5 | ![notify](https://user-images.githubusercontent.com/24252670/130856848-e8289850-028f-4f49-82f1-5ea1b8912f5e.gif)
  6 | 
  7 | 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.
  8 | 
  9 | * [Installation](#Installation)
 10 | * [Usage](#usage)
 11 |   - [Viewing History](#viewing-history)
 12 | * [Configuration](#configuration)
 13 |   - [Setup](#setup)
 14 |   - [Highlights](#highlights)
 15 |   - [Render Style](#render-style)
 16 |   - [Animation Style](#animation-style)
 17 |     + [Opening the window](#opening-the-window)
 18 |     + [Changing the window](#changing-the-window)
 19 | 
 20 | ## Installation
 21 | 
 22 | ### Prerequisites
 23 | 
 24 | Make sure to use a font which supported glyphs (icons), font can be found [here](https://github.com/ryanoasis/nerd-fonts).
 25 | 
 26 | 24-bit colour is required, which can be enabled by adding this to your init.lua
 27 | ```lua
 28 | vim.opt.termguicolors = true
 29 | ```
 30 | 
 31 | Then you can install nvim-notify with the package manager of your choice.
 32 | 
 33 | [**dein**](https://github.com/Shougo/dein.vim):
 34 | 
 35 | ```vim
 36 | call dein#add("rcarriga/nvim-notify")
 37 | ```
 38 | 
 39 | [**vim-plug**](https://github.com/junegunn/vim-plug):
 40 | 
 41 | ```vim
 42 | Plug 'rcarriga/nvim-notify'
 43 | ```
 44 | 
 45 | [**packer**](https://github.com/wbthomason/packer.nvim):
 46 | 
 47 | ```lua
 48 | use 'rcarriga/nvim-notify'
 49 | ```
 50 | 
 51 | ## Usage
 52 | 
 53 | Simply call the module with a message!
 54 | 
 55 | ```lua
 56 | require("notify")("My super important message")
 57 | ```
 58 | 
 59 | Other plugins can use the notification windows by setting it as your default notify function
 60 | 
 61 | ```lua
 62 | vim.notify = require("notify")
 63 | ```
 64 | 
 65 | You can supply a level to change the border highlighting
 66 | 
 67 | ```lua
 68 | vim.notify("This is an error message", "error")
 69 | ```
 70 | 
 71 | Updating an existing notification is also possible!
 72 | 
 73 | ![](https://user-images.githubusercontent.com/24252670/152641078-92f3da72-f49f-4705-aec8-86512693445f.gif)
 74 | 
 75 | 
 76 | Use treesitter highlighting inside notifications with opacity changing
 77 | 
 78 | ![](https://user-images.githubusercontent.com/24252670/165042795-565878a3-9c6d-4c0b-ab0d-6858515835c5.gif)
 79 | 
 80 | There are a number of custom options that can be supplied in a table as the third argument.
 81 | See `:h NotifyOptions` for details.
 82 | 
 83 | Sample code for the first GIF above:
 84 | 
 85 | ```lua
 86 | local plugin = "My Awesome Plugin"
 87 | 
 88 | vim.notify("This is an error message.\nSomething went wrong!", "error", {
 89 |   title = plugin,
 90 |   on_open = function()
 91 |     vim.notify("Attempting recovery.", vim.log.levels.WARN, {
 92 |       title = plugin,
 93 |     })
 94 |     local timer = vim.loop.new_timer()
 95 |     timer:start(2000, 0, function()
 96 |       vim.notify({ "Fixing problem.", "Please wait..." }, "info", {
 97 |         title = plugin,
 98 |         timeout = 3000,
 99 |         on_close = function()
100 |           vim.notify("Problem solved", nil, { title = plugin })
101 |           vim.notify("Error code 0x0395AF", 1, { title = plugin })
102 |         end,
103 |       })
104 |     end)
105 |   end,
106 | })
107 | ```
108 | 
109 | You can also use plenary's async library to avoid using callbacks:
110 | ```lua
111 | local async = require("plenary.async")
112 | local notify = require("notify").async
113 | 
114 | async.run(function()
115 |   notify("Let's wait for this to close").events.close()
116 |   notify("It closed!")
117 | end)
118 | ```
119 | 
120 | Set a custom filetype to take advantage of treesitter highlighting:
121 | 
122 | ```lua
123 | vim.notify(text, "info", {
124 |   title = "My Awesome Plugin",
125 |   on_open = function(win)
126 |     local buf = vim.api.nvim_win_get_buf(win)
127 |     vim.api.nvim_buf_set_option(buf, "filetype", "markdown")
128 |   end,
129 | })
130 | ```
131 | 
132 | 
133 | Check out the wiki for more examples!
134 | 
135 | ### Viewing History
136 | 
137 | If you have [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) installed then you can use the `notify` extension to search the history:
138 | 
139 | ```vim
140 | :Telescope notify
141 | ```
142 | or in lua
143 | ```lua
144 | require('telescope').extensions.notify.notify(<opts>)
145 | ```
146 | 
147 | **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.
148 | 
149 | <p align="center">
150 |   <img src="https://user-images.githubusercontent.com/24252670/136264308-2fcdfe57-a8f6-4b34-8ea1-e3a8349bc581.png" />
151 | </p>
152 | 
153 | There is a command to display a log of the history.
154 | 
155 | ```vim
156 | :Notifications
157 | ```
158 | 
159 | <p align="center">
160 |   <img src="https://user-images.githubusercontent.com/24252670/136264653-83038119-634b-48e7-8e8a-0edf4546efe2.png" />
161 | </p>
162 | 
163 | You can get a list of past notifications with the history function
164 | ```lua
165 | require("notify").history()
166 | ```
167 | 
168 | There is another command to clear the history.
169 | 
170 | ```vim
171 | :NotificationsClear
172 | ```
173 | 
174 | You can clear the notifications with the clear history function
175 | 
176 | ```lua
177 | require("notify").clear_history()
178 | ```
179 | 
180 | ## Configuration
181 | 
182 | ### Setup
183 | 
184 | You can optionally call the `setup` function to provide configuration options
185 | 
186 | See `:h notify.Config` for options and `:h notify.setup()` for default values.
187 | 
188 | ### Highlights
189 | 
190 | You can define custom highlights by supplying highlight groups for each of the levels.
191 | The naming scheme follows a simple structure: `Notify<upper case level name><section>`.
192 | If you want to use custom levels, you can define the highlights for them or
193 | they will follow the `INFO` highlights by default.
194 | 
195 | Here are the defaults:
196 | 
197 | ```vim
198 | highlight NotifyERRORBorder guifg=#8A1F1F
199 | highlight NotifyWARNBorder guifg=#79491D
200 | highlight NotifyINFOBorder guifg=#4F6752
201 | highlight NotifyDEBUGBorder guifg=#8B8B8B
202 | highlight NotifyTRACEBorder guifg=#4F3552
203 | highlight NotifyERRORIcon guifg=#F70067
204 | highlight NotifyWARNIcon guifg=#F79000
205 | highlight NotifyINFOIcon guifg=#A9FF68
206 | highlight NotifyDEBUGIcon guifg=#8B8B8B
207 | highlight NotifyTRACEIcon guifg=#D484FF
208 | highlight NotifyERRORTitle  guifg=#F70067
209 | highlight NotifyWARNTitle guifg=#F79000
210 | highlight NotifyINFOTitle guifg=#A9FF68
211 | highlight NotifyDEBUGTitle  guifg=#8B8B8B
212 | highlight NotifyTRACETitle  guifg=#D484FF
213 | highlight link NotifyERRORBody Normal
214 | highlight link NotifyWARNBody Normal
215 | highlight link NotifyINFOBody Normal
216 | highlight link NotifyDEBUGBody Normal
217 | highlight link NotifyTRACEBody Normal
218 | ```
219 | 
220 | ### Render Style
221 | 
222 | The appearance of the notifications can be configured, using either built-in methods or custom functions.
223 | See `:help notify-render()` for details
224 | 
225 | 1. "default"
226 | 
227 | ![default](https://user-images.githubusercontent.com/24252670/141534868-fdcc9d03-9f7b-47fd-acfc-5a20b98e4e0a.png)
228 | 
229 | 2. "minimal"
230 | 
231 | ![image](https://user-images.githubusercontent.com/24252670/141534952-bb0cf491-5bb4-473c-9a67-8adb5b23b232.png)
232 | 
233 | 3. "simple"
234 | 
235 | ![image](https://user-images.githubusercontent.com/24252670/191683325-220208a0-90bf-4daa-b375-01b573ca524c.png)
236 | 
237 | 4. "compact"
238 | 
239 | ![image](https://user-images.githubusercontent.com/24252670/212632432-86621888-f885-4074-aed4-d12b5e291ab2.png)
240 | 
241 | 5. "wrapped-compact"
242 | 
243 | Mostly same as `compact`, but lines are wrapped based on `max_width`, some padding is added.
244 | 
245 | ![image](https://github.com/rcarriga/nvim-notify/assets/73286100/72237d45-6e3b-4c2a-8010-513a26871682)
246 | 
247 | 5. "wrapped-default"
248 | 
249 | Similar to `default`, but lines are wrapped based on `max_width`, some padding is added.
250 | 
251 | Feel free to submit custom rendering functions to share with others!
252 | 
253 | ### Animation Style
254 | 
255 | The animation is designed to work in stages. The first stage is the opening of
256 | the window, and all subsequent stages can changes the position or opacity of
257 | the window. You can use one of the built-in styles or provide your own in the setup.
258 | 
259 | 1. "fade_in_slide_out"
260 | 
261 | ![fade_slide](https://user-images.githubusercontent.com/24252670/130924913-f3a61f2c-2330-4426-a787-3cd7494fccc0.gif)
262 | 
263 | 2. "fade"
264 | 
265 | ![fade](https://user-images.githubusercontent.com/24252670/130924911-a89bef9b-e815-4aa5-a255-84bc23dd8c8e.gif)
266 | 
267 | 3. "slide"
268 | 
269 | ![slide](https://user-images.githubusercontent.com/24252670/130924905-656cabfc-9eb7-4e22-b6da-8a2a1f508fa5.gif)
270 | 
271 | 4. "static"
272 | 
273 | ![static](https://user-images.githubusercontent.com/24252670/130924902-8c77b5a1-6d13-48f4-98a9-866e58cb76e4.gif)
274 | 
275 | Custom styles can be provided by setting the config `stages` value to a list of
276 | functions.
277 | 
278 | If you create a custom style, feel free to open a PR to submit it as a built-in style!
279 | 
280 | **NB.** This is a prototype API that is open to change. I am looking for
281 | feedback on both issues or extra data that could be useful in creating
282 | animation styles.
283 | 
284 | Check the [built-in styles](./lua/notify/stages/) to see examples
285 | 
286 | #### Opening the window
287 | 
288 | The first function in the list should return a table to be provided to
289 | `nvim_open_win`, optionally including an extra `opacity` key which can be
290 | between 0-100.
291 | 
292 | The function is given a state table that contains the following keys:
293 | 
294 | - `message: table` State of the message to be shown
295 |   - `width` Width of the message buffer
296 |   - `height` Height of the message buffer
297 | - `open_windows: integer[]` List of all window IDs currently showing messages
298 | - `buffer: integer` The buffer containing the rendered notification message.
299 | 
300 | If a notification can't be shown at the moment the function should return `nil`.
301 | 
302 | #### Changing the window
303 | 
304 | All following functions should return the goal values for the window to reach from it's current point.
305 | They will receive the same state object as the initial function and a second argument of the window ID.
306 | 
307 | The following fields can be returned in a table:
308 | - `col`
309 | - `row`
310 | - `height`
311 | - `width`
312 | - `opacity`
313 | 
314 | These can be provided as either numbers or as a table. If they are
315 | provided as numbers then they will change instantly the value given.
316 | 
317 | If they are provided as a table, they will be treated as a value to animate towards.
318 | This uses a dampened spring algorithm to provide a natural feel to the movement.
319 | 
320 | The table must contain the goal value as the 1st index (e.g. `{10}`)
321 | 
322 | All other values are provided with keys:
323 | 
324 | - `damping: number` How motion decays over time. Values less than 1 mean the spring can overshoot.
325 |   - Bounds: >= 0
326 |   - Default: 1
327 | - `frequency: number` How fast the spring oscillates
328 |   - Bounds: >= 0
329 |   - Default: 1
330 | - `complete: fun(value: number): bool` Function to determine if value has reached its goal. If not
331 |   provided it will complete when the value rounded to 2 decimal places is equal
332 |   to the goal.
333 | 
334 | Once the last function has reached its goals, the window is removed.
335 | 
336 | One of the stages should also return the key `time` set to true. This is
337 | treated as the stage which the notification is on a timer. The goals of this
338 | stage are not used to check if it is complete. The next stage will start
339 | once the notification reaches its timeout.
340 | 


--------------------------------------------------------------------------------
/doc/nvim-notify.txt:
--------------------------------------------------------------------------------
  1 | *nvim-notify.txt*   A fancy, configurable notification manager for NeoVim
  2 | 
  3 | ==============================================================================
  4 | 
  5 | A fancy, configurable notification manager for NeoVim
  6 | 
  7 | notify                                                                  *notify*
  8 | 
  9 | 
 10 |                                                                 *notify.setup()*
 11 | `setup`({user_config})
 12 | 
 13 | Configure nvim-notify
 14 |    See: ~
 15 |        |notify.Config|
 16 |        |notify-render|
 17 | 
 18 | Parameters~
 19 | {user_config} `(notify.Config|nil)`
 20 | Default values:
 21 | >lua
 22 |   {
 23 |     background_colour = "NotifyBackground",
 24 |     fps = 30,
 25 |     icons = {
 26 |       DEBUG = "",
 27 |       ERROR = "",
 28 |       INFO = "",
 29 |       TRACE = "✎",
 30 |       WARN = ""
 31 |     },
 32 |     level = 2,
 33 |     minimum_width = 50,
 34 |     render = "default",
 35 |     stages = "fade_in_slide_out",
 36 |     time_formats = {
 37 |       notification = "%T",
 38 |       notification_history = "%FT%T"
 39 |     },
 40 |     timeout = 5000,
 41 |     top_down = true
 42 |   }
 43 | <
 44 | 
 45 |                                                                 *notify.Options*
 46 | Options for an individual notification
 47 | Fields~
 48 | {title} `(string)`
 49 | {icon} `(string)`
 50 | {timeout} `(number|boolean)` Time to show notification in milliseconds, set to false to disable timeout.
 51 | {on_open} `(function)` Callback for when window opens, receives window as argument.
 52 | {on_close} `(function)` Callback for when window closes, receives window as argument.
 53 | {keep} `(function)` Function to keep the notification window open after timeout, should return boolean.
 54 | {render} `(function|string)` Function to render a notification buffer.
 55 | {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.
 56 | {hide_from_history} `(boolean)` Hide this notification from the history
 57 | {animate} `(boolean)` If false, the window will jump to the timed stage. Intended for use in blocking events (e.g. vim.fn.input)
 58 | 
 59 |                                                                  *notify.Events*
 60 | Async events for a notification
 61 | Fields~
 62 | {open} `(function)` Resolves when notification is opened
 63 | {close} `(function)` Resolved when notification is closed
 64 | 
 65 |                                                                  *notify.Record*
 66 | Record of a previously sent notification
 67 | Fields~
 68 | {id} `(integer)`
 69 | {message} `(string[])` Lines of the message
 70 | {level} `(string|integer)` Log level. See vim.log.levels
 71 | {title} `(string[])` Left and right sections of the title
 72 | {icon} `(string)` Icon used for notification
 73 | {time} `(number)` Time of message, as returned by `vim.fn.localtime()`
 74 | {render} `(function)` Function to render notification buffer
 75 | 
 76 |                                                             *notify.AsyncRecord*
 77 | Inherits: `notify.Record`
 78 | 
 79 | Fields~
 80 | {events} `(notify.Events)`
 81 | 
 82 |                                                                *notify.notify()*
 83 | `notify`({message}, {level}, {opts})
 84 | 
 85 | Display a notification.
 86 | 
 87 | You can call the module directly rather than using this:
 88 | >lua
 89 |  require("notify")(message, level, opts)
 90 | <
 91 | Parameters~
 92 | {message} `(string|string[])` Notification message
 93 | {level} `(string|number)` Log level. See vim.log.levels
 94 | {opts} `(notify.Options)` Notification options
 95 | Return~
 96 | `(notify.Record)`
 97 | 
 98 |                                                                 *notify.async()*
 99 | `async`({message}, {level}, {opts})
100 | 
101 | Display a notification asynchronously
102 | 
103 | This uses plenary's async library, allowing a cleaner interface for
104 | open/close events. You must call this function within an async context.
105 | 
106 | The `on_close` and `on_open` options are not used.
107 | 
108 | Parameters~
109 | {message} `(string|string[])` Notification message
110 | {level} `(string|number)` Log level. See vim.log.levels
111 | {opts} `(notify.Options)` Notification options
112 | Return~
113 | `(notify.AsyncRecord)`
114 | 
115 |                                                               *notify.history()*
116 | `history`({opts})
117 | 
118 | Get records of all previous notifications
119 | 
120 | You can use the `:Notifications` command to display a log of previous notifications
121 | Parameters~
122 | {opts?} `(notify.HistoryOpts)`
123 | Return~
124 | `(notify.Record[])`
125 | 
126 |                                                             *notify.HistoryOpts*
127 | Fields~
128 | {include_hidden} `(boolean)` Include notifications hidden from history
129 | 
130 |                                                               *notify.dismiss()*
131 | `dismiss`({opts})
132 | 
133 | Dismiss all notification windows currently displayed
134 | Parameters~
135 | {opts} `(notify.DismissOpts)`
136 | 
137 |                                                             *notify.DismissOpts*
138 | Fields~
139 | {pending} `(boolean)` Clear pending notifications
140 | {silent} `(boolean)` Suppress notification that pending notifications were dismissed.
141 | 
142 |                                                                  *notify.open()*
143 | `open`({notif_id}, {opts})
144 | 
145 | Open a notification in a new buffer
146 | Parameters~
147 | {notif_id} `(integer|notify.Record)`
148 | {opts} `(notify.OpenOpts)`
149 | Return~
150 | `(notify.OpenedBuffer)`
151 | 
152 |                                                                *notify.OpenOpts*
153 | Fields~
154 | {buffer} `(integer)` Use this buffer, instead of creating a new one
155 | {max_width} `(integer)` Render message to this width (used to limit window decoration sizes)
156 | 
157 |                                                            *notify.OpenedBuffer*
158 | Fields~
159 | {buffer} `(integer)` Created buffer number
160 | {height} `(integer)` Height of the buffer content including extmarks
161 | {width} `(integer)` width of the buffer content including extmarks
162 | {highlights} `(table<string, string>)` Highlights used for the buffer contents
163 | 
164 |                                                               *notify.pending()*
165 | `pending`()
166 | 
167 | Number of notifications currently waiting to be displayed
168 | Return~
169 | `(integer[])`
170 | 
171 |                                                              *notify.instance()*
172 | `instance`({user_config}, {inherit})
173 | 
174 | Configure an instance of nvim-notify.
175 | You can use this to manage a separate instance of nvim-notify with completely different configuration.
176 | The returned instance will have the same functions as the notify module.
177 | Parameters~
178 | {user_config} `(notify.Config)`
179 | {inherit?} `(boolean)` Inherit the global configuration, default true
180 | 
181 | 
182 | ==============================================================================
183 | notify.config                                                    *notify.config*
184 | 
185 |                                                                  *notify.Config*
186 | Fields~
187 | {level} `(string|integer)` Minimum log level to display. See vim.log.levels.
188 | {timeout} `(number)` Default timeout for notification
189 | {max_width} `(number|function)` Max number of columns for messages
190 | {max_height} `(number|function)` Max number of lines for a message
191 | {stages} `(string|function[])` Animation stages
192 | {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
193 | {icons} `(table)` Icons for each level (upper case names)
194 | {time_formats} `(table)` Time formats for different kind of notifications
195 | {on_open} `(function)` Function called when a new window is opened, use for changing win settings/config
196 | {on_close} `(function)` Function called when a window is closed
197 | {render} `(function|string)` Function to render a notification buffer or a built-in renderer name
198 | {minimum_width} `(integer)` Minimum width for notification windows
199 | {fps} `(integer)` Frames per second for animation stages, higher value means smoother animations but more CPU usage
200 | {top_down} `(boolean)` whether or not to position the notifications at the top or not
201 | 
202 | 
203 | ==============================================================================
204 | notify-render                                                    *notify-render*
205 | 
206 | Notification buffer rendering
207 | 
208 | Custom rendering can be provided by both the user config in the setup or on
209 | an individual notification using the `render` key.
210 | The key can either be the name of a built-in renderer or a custom function.
211 | 
212 | Built-in renderers:
213 | - `"default"`
214 | - `"minimal"`
215 | - `"simple"`
216 | - `"compact"`
217 | - `"wrapped-compact"`
218 | 
219 | Custom functions should accept a buffer, a notification record and a highlights table
220 | 
221 | >
222 |     render: fun(buf: integer, notification: notify.Record, highlights: notify.Highlights, config)
223 | <
224 | You should use the provided highlight groups to take advantage of opacity
225 | changes as they will be updated as the notification is animated
226 | 
227 |                                                              *notify.Highlights*
228 | Fields~
229 | {title} `(string)`
230 | {icon} `(string)`
231 | {border} `(string)`
232 | {body} `(string)`
233 | 
234 | 
235 |  vim:tw=78:ts=8:noet:ft=help:norl:


--------------------------------------------------------------------------------
/lua/notify/animate/init.lua:
--------------------------------------------------------------------------------
1 | local M = {}
2 | 
3 | M.spring = require("notify.animate.spring")
4 | 
5 | return M
6 | 


--------------------------------------------------------------------------------
/lua/notify/animate/spring.lua:
--------------------------------------------------------------------------------
 1 | -- Adapted from https://gist.github.com/Fraktality/1033625223e13c01aa7144abe4aaf54d
 2 | -- Explanation found here https://www.ryanjuckett.com/damped-springs/
 3 | local pi = math.pi
 4 | local exp = math.exp
 5 | local sin = math.sin
 6 | local cos = math.cos
 7 | local sqrt = math.sqrt
 8 | 
 9 | ---@class SpringState
10 | ---@field position number
11 | ---@field velocity number | nil
12 | 
13 | ---@param dt number @Step in time
14 | ---@param state SpringState
15 | return function(dt, goal, state, frequency, damping)
16 |   local angular_freq = frequency * 2 * pi
17 | 
18 |   local cur_vel = state.velocity or 0
19 | 
20 |   local offset = state.position - goal
21 |   local decay = exp(-dt * damping * angular_freq)
22 | 
23 |   local new_pos
24 |   local new_vel
25 | 
26 |   if damping == 1 then -- critically damped
27 |     new_pos = (cur_vel * dt + offset * (angular_freq * dt + 1)) * decay + goal
28 |     new_vel = (cur_vel - angular_freq * dt * (offset * angular_freq + cur_vel)) * decay
29 |   elseif damping < 1 then -- underdamped
30 |     local c = sqrt(1 - damping * damping)
31 | 
32 |     local i = cos(angular_freq * c * dt)
33 |     local j = sin(angular_freq * c * dt)
34 | 
35 |     new_pos = (i * offset + j * (cur_vel + damping * angular_freq * offset) / (angular_freq * c))
36 |         * decay
37 |       + goal
38 |     new_vel = (i * c * cur_vel - j * (cur_vel * damping + angular_freq * offset)) * decay / c
39 |   else -- overdamped
40 |     local c = sqrt(damping * damping - 1)
41 | 
42 |     local r1 = -angular_freq * (damping - c)
43 |     local r2 = -angular_freq * (damping + c)
44 | 
45 |     local co2 = (cur_vel - r1 * offset) / (2 * angular_freq * c)
46 |     local co1 = offset - co2
47 | 
48 |     local e1 = co1 * exp(r1 * dt)
49 |     local e2 = co2 * exp(r2 * dt)
50 | 
51 |     new_pos = e1 + e2 + goal
52 |     new_pos = r1 * e1 + r2 * e2
53 |   end
54 |   state.position = new_pos
55 |   state.velocity = new_vel
56 | end
57 | 


--------------------------------------------------------------------------------
/lua/notify/config/highlights.lua:
--------------------------------------------------------------------------------
 1 | local M = {}
 2 | 
 3 | function M.setup()
 4 |   vim.cmd([[
 5 |     hi default link NotifyBackground Normal
 6 |     hi default NotifyERRORBorder guifg=#8A1F1F
 7 |     hi default NotifyWARNBorder guifg=#79491D
 8 |     hi default NotifyINFOBorder guifg=#4F6752
 9 |     hi default NotifyDEBUGBorder guifg=#8B8B8B
10 |     hi default NotifyTRACEBorder guifg=#4F3552
11 |     hi default NotifyERRORIcon guifg=#F70067
12 |     hi default NotifyWARNIcon guifg=#F79000
13 |     hi default NotifyINFOIcon guifg=#A9FF68
14 |     hi default NotifyDEBUGIcon guifg=#8B8B8B
15 |     hi default NotifyTRACEIcon guifg=#D484FF
16 |     hi default NotifyERRORTitle  guifg=#F70067
17 |     hi default NotifyWARNTitle guifg=#F79000
18 |     hi default NotifyINFOTitle guifg=#A9FF68
19 |     hi default NotifyDEBUGTitle  guifg=#8B8B8B
20 |     hi default NotifyTRACETitle  guifg=#D484FF
21 |     hi default link NotifyERRORBody Normal
22 |     hi default link NotifyWARNBody Normal
23 |     hi default link NotifyINFOBody Normal
24 |     hi default link NotifyDEBUGBody Normal
25 |     hi default link NotifyTRACEBody Normal
26 | 
27 |     hi default link NotifyLogTime Comment
28 |     hi default link NotifyLogTitle Special
29 |   ]])
30 | end
31 | 
32 | M.setup()
33 | 
34 | vim.cmd([[
35 |   augroup NvimNotifyRefreshHighlights
36 |     autocmd!
37 |     autocmd ColorScheme * lua require('notify.config.highlights').setup()
38 |   augroup END
39 | ]])
40 | 
41 | return M
42 | 


--------------------------------------------------------------------------------
/lua/notify/config/init.lua:
--------------------------------------------------------------------------------
  1 | ---@tag notify.config
  2 | 
  3 | local Config = {}
  4 | local util = require("notify.util")
  5 | 
  6 | require("notify.config.highlights")
  7 | 
  8 | local BUILTIN_RENDERERS = {
  9 |   DEFAULT = "default",
 10 |   MINIMAL = "minimal",
 11 | }
 12 | 
 13 | local BUILTIN_STAGES = {
 14 |   FADE = "fade",
 15 |   SLIDE = "slide",
 16 |   SLIDE_OUT = "slide_out",
 17 |   FADE_IN_SLIDE_OUT = "fade_in_slide_out",
 18 |   STATIC = "static",
 19 | }
 20 | 
 21 | local default_config = {
 22 |   level = vim.log.levels.INFO,
 23 |   timeout = 5000,
 24 |   max_width = nil,
 25 |   max_height = nil,
 26 |   stages = BUILTIN_STAGES.FADE_IN_SLIDE_OUT,
 27 |   render = BUILTIN_RENDERERS.DEFAULT,
 28 |   background_colour = "NotifyBackground",
 29 |   on_open = nil,
 30 |   on_close = nil,
 31 |   minimum_width = 50,
 32 |   fps = 30,
 33 |   top_down = true,
 34 |   merge_duplicates = true,
 35 |   time_formats = {
 36 |     notification_history = "%FT%T",
 37 |     notification = "%T",
 38 |   },
 39 |   icons = {
 40 |     ERROR = "",
 41 |     WARN = "",
 42 |     INFO = "",
 43 |     DEBUG = "",
 44 |     TRACE = "✎",
 45 |   },
 46 | }
 47 | 
 48 | ---@class notify.Config
 49 | ---@field level string|integer|nil Minimum log level to display. See vim.log.levels.
 50 | ---@field timeout number? Default timeout for notification
 51 | ---@field max_width number|function|nil Max number of columns for messages
 52 | ---@field max_height number|function|nil Max number of lines for a message
 53 | ---@field stages string|function[]|nil Animation stages
 54 | ---@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
 55 | ---@field icons table? Icons for each level (upper case names)
 56 | ---@field time_formats table? Time formats for different kind of notifications
 57 | ---@field on_open function? Function called when a new window is opened, use for changing win settings/config
 58 | ---@field on_close function? Function called when a window is closed
 59 | ---@field render function|string|nil Function to render a notification buffer or a built-in renderer name
 60 | ---@field minimum_width integer? Minimum width for notification windows
 61 | ---@field fps integer? Frames per second for animation stages, higher value means smoother animations but more CPU usage
 62 | ---@field top_down boolean? whether or not to position the notifications at the top or not
 63 | ---@field merge_duplicates boolean|integer whether to replace visible notification if new one is the same, can be an integer for min duplicate count
 64 | 
 65 | local opacity_warned = false
 66 | 
 67 | local function validate_highlight(colour_or_group, needs_opacity)
 68 |   if type(colour_or_group) == "function" then
 69 |     return colour_or_group
 70 |   end
 71 |   if colour_or_group:sub(1, 1) == "#" then
 72 |     return function()
 73 |       return colour_or_group
 74 |     end
 75 |   end
 76 |   return function()
 77 |     local group = vim.api.nvim_get_hl(0, { name = colour_or_group, create = false, link = false })
 78 |     if not group or not group.bg then
 79 |       if needs_opacity and not opacity_warned then
 80 |         opacity_warned = true
 81 |         vim.schedule(function()
 82 |           vim.notify("Highlight group '" .. colour_or_group .. [[' has no background highlight
 83 | Please provide an RGB hex value or highlight group with a background value for 'background_colour' option.
 84 | This is the colour that will be used for 100% transparency.
 85 | ```lua
 86 | require("notify").setup({
 87 |   background_colour = "#000000",
 88 | })
 89 | ```
 90 | Defaulting to #000000]], "warn", {
 91 |             title = "nvim-notify",
 92 |             on_open = function(win)
 93 |               local buf = vim.api.nvim_win_get_buf(win)
 94 |               vim.api.nvim_set_option_value("filetype", "markdown", { buf = buf })
 95 |             end,
 96 |           })
 97 |         end)
 98 |       end
 99 |       return "#000000"
100 |     end
101 |     return string.format("#%x", group.bg)
102 |   end
103 | end
104 | 
105 | function Config._format_default()
106 |   local lines = { "Default values:", ">lua" }
107 |   for line in vim.gsplit(vim.inspect(default_config), "\n", true) do
108 |     table.insert(lines, "  " .. line)
109 |   end
110 |   table.insert(lines, "<")
111 |   return lines
112 | end
113 | 
114 | function Config.setup(custom_config)
115 |   local user_config = vim.tbl_deep_extend("keep", custom_config or {}, default_config)
116 |   local config = {}
117 | 
118 |   function config.merged()
119 |     return user_config
120 |   end
121 | 
122 |   function config.level()
123 |     local level = user_config.level
124 |     if type(level) == "number" then
125 |       return level
126 |     end
127 |     return vim.log.levels[vim.fn.toupper(level)] or vim.log.levels.INFO
128 |   end
129 | 
130 |   function config.fps()
131 |     return user_config.fps
132 |   end
133 | 
134 |   function config.background_colour()
135 |     return tonumber(user_config.background_colour():gsub("#", "0x"), 16)
136 |   end
137 | 
138 |   function config.time_formats()
139 |     return user_config.time_formats
140 |   end
141 | 
142 |   function config.icons()
143 |     return user_config.icons
144 |   end
145 | 
146 |   function config.stages()
147 |     return user_config.stages
148 |   end
149 | 
150 |   function config.default_timeout()
151 |     return user_config.timeout
152 |   end
153 | 
154 |   function config.on_open()
155 |     return user_config.on_open
156 |   end
157 | 
158 |   function config.top_down()
159 |     return user_config.top_down
160 |   end
161 | 
162 |   function config.merge_duplicates()
163 |     return user_config.merge_duplicates
164 |   end
165 | 
166 |   function config.on_close()
167 |     return user_config.on_close
168 |   end
169 | 
170 |   function config.render()
171 |     return user_config.render
172 |   end
173 | 
174 |   function config.minimum_width()
175 |     return user_config.minimum_width
176 |   end
177 | 
178 |   function config.max_width()
179 |     return util.is_callable(user_config.max_width) and user_config.max_width()
180 |       or user_config.max_width
181 |   end
182 | 
183 |   function config.max_height()
184 |     return util.is_callable(user_config.max_height) and user_config.max_height()
185 |       or user_config.max_height
186 |   end
187 | 
188 |   local stages = config.stages()
189 | 
190 |   local needs_opacity =
191 |     vim.tbl_contains({ BUILTIN_STAGES.FADE_IN_SLIDE_OUT, BUILTIN_STAGES.FADE }, stages)
192 | 
193 |   if needs_opacity and not vim.opt.termguicolors:get() and vim.fn.has("nvim-0.10") == 0 then
194 |     user_config.stages = BUILTIN_STAGES.STATIC
195 |     vim.schedule(function()
196 |       vim.notify(
197 |         "Opacity changes require termguicolors to be set.\nChange to different animation stages or set termguicolors to disable this warning",
198 |         "warn",
199 |         { title = "nvim-notify" }
200 |       )
201 |     end)
202 |   end
203 | 
204 |   user_config.background_colour = validate_highlight(user_config.background_colour, needs_opacity)
205 | 
206 |   return config
207 | end
208 | 
209 | return Config
210 | 


--------------------------------------------------------------------------------
/lua/notify/init.lua:
--------------------------------------------------------------------------------
  1 | ---@text
  2 | --- A fancy, configurable notification manager for NeoVim
  3 | 
  4 | local config = require("notify.config")
  5 | local instance = require("notify.instance")
  6 | 
  7 | ---@class notify
  8 | local notify = {}
  9 | 
 10 | local global_instance, global_config
 11 | 
 12 | --- Configure nvim-notify
 13 | ---    See: ~
 14 | ---        |notify.Config|
 15 | ---        |notify-render|
 16 | ---
 17 | ---@param user_config notify.Config|nil
 18 | ---@eval return require('notify.config')._format_default()
 19 | function notify.setup(user_config)
 20 |   global_instance, global_config = notify.instance(user_config)
 21 |   local has_telescope = (vim.fn.exists("g:loaded_telescope") == 1)
 22 |   if has_telescope then
 23 |     require("telescope").load_extension("notify")
 24 |   end
 25 |   vim.cmd([[command! Notifications :lua require("notify")._print_history()<CR>]])
 26 |   vim.cmd([[command! NotificationsClear :lua require("notify").clear_history()<CR>]])
 27 | end
 28 | 
 29 | function notify._config()
 30 |   return config.setup(global_config)
 31 | end
 32 | 
 33 | ---@class notify.Options
 34 | --- Options for an individual notification
 35 | ---@field title string
 36 | ---@field icon string
 37 | ---@field timeout number|boolean Time to show notification in milliseconds, set to false to disable timeout.
 38 | ---@field on_open function Callback for when window opens, receives window as argument.
 39 | ---@field on_close function Callback for when window closes, receives window as argument.
 40 | ---@field keep function Function to keep the notification window open after timeout, should return boolean.
 41 | ---@field render function|string Function to render a notification buffer.
 42 | ---@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.
 43 | ---@field hide_from_history boolean Hide this notification from the history
 44 | ---@field animate boolean If false, the window will jump to the timed stage. Intended for use in blocking events (e.g. vim.fn.input)
 45 | 
 46 | ---@class notify.Events
 47 | --- Async events for a notification
 48 | ---@field open function Resolves when notification is opened
 49 | ---@field close function Resolved when notification is closed
 50 | 
 51 | ---@class notify.Record
 52 | --- Record of a previously sent notification
 53 | ---@field id integer
 54 | ---@field message string[] Lines of the message
 55 | ---@field level string|integer Log level. See vim.log.levels
 56 | ---@field title string[] Left and right sections of the title
 57 | ---@field icon string Icon used for notification
 58 | ---@field time number Time of message, as returned by `vim.fn.localtime()`
 59 | ---@field render function Function to render notification buffer
 60 | 
 61 | ---@class notify.AsyncRecord : notify.Record
 62 | ---@field events notify.Events
 63 | 
 64 | --- Display a notification.
 65 | ---
 66 | --- You can call the module directly rather than using this:
 67 | --- >lua
 68 | ---  require("notify")(message, level, opts)
 69 | --- <
 70 | ---@param message string|string[] Notification message
 71 | ---@param level string|number Log level. See vim.log.levels
 72 | ---@param opts notify.Options Notification options
 73 | ---@return notify.Record
 74 | function notify.notify(message, level, opts)
 75 |   if not global_instance then
 76 |     notify.setup()
 77 |   end
 78 |   return global_instance.notify(message, level, opts)
 79 | end
 80 | 
 81 | --- Display a notification asynchronously
 82 | ---
 83 | --- This uses plenary's async library, allowing a cleaner interface for
 84 | --- open/close events. You must call this function within an async context.
 85 | ---
 86 | --- The `on_close` and `on_open` options are not used.
 87 | ---
 88 | ---@param message string|string[] Notification message
 89 | ---@param level string|number Log level. See vim.log.levels
 90 | ---@param opts notify.Options Notification options
 91 | ---@return notify.AsyncRecord
 92 | function notify.async(message, level, opts)
 93 |   if not global_instance then
 94 |     notify.setup()
 95 |   end
 96 |   return global_instance.async(message, level, opts)
 97 | end
 98 | 
 99 | --- Get records of all previous notifications
100 | ---
101 | --- You can use the `:Notifications` command to display a log of previous notifications
102 | ---@param opts? notify.HistoryOpts
103 | ---@return notify.Record[]
104 | function notify.history(opts)
105 |   if not global_instance then
106 |     notify.setup()
107 |   end
108 |   return global_instance.history(opts)
109 | end
110 | 
111 | ---@class notify.HistoryOpts
112 | ---@field include_hidden boolean Include notifications hidden from history
113 | 
114 | --- Clear records of all previous notifications
115 | ---
116 | --- You can use the `:NotificationsClear` command to clear the log of previous notifications
117 | function notify.clear_history()
118 |   if not global_instance then
119 |     notify.setup()
120 |   end
121 |   return global_instance.clear_history()
122 | end
123 | 
124 | --- Dismiss all notification windows currently displayed
125 | ---@param opts notify.DismissOpts
126 | function notify.dismiss(opts)
127 |   if not global_instance then
128 |     notify.setup()
129 |   end
130 |   return global_instance.dismiss(opts)
131 | end
132 | 
133 | ---@class notify.DismissOpts
134 | ---@field pending boolean Clear pending notifications
135 | ---@field silent boolean Suppress notification that pending notifications were dismissed.
136 | 
137 | --- Open a notification in a new buffer
138 | ---@param notif_id integer|notify.Record
139 | ---@param opts notify.OpenOpts
140 | ---@return notify.OpenedBuffer
141 | function notify.open(notif_id, opts)
142 |   if not global_instance then
143 |     notify.setup()
144 |   end
145 |   return global_instance.open(notif_id, opts)
146 | end
147 | 
148 | ---@class notify.OpenOpts
149 | ---@field buffer integer Use this buffer, instead of creating a new one
150 | ---@field max_width integer Render message to this width (used to limit window decoration sizes)
151 | 
152 | ---@class notify.OpenedBuffer
153 | ---@field buffer integer Created buffer number
154 | ---@field height integer Height of the buffer content including extmarks
155 | ---@field width integer width of the buffer content including extmarks
156 | ---@field highlights table<string, string> Highlights used for the buffer contents
157 | 
158 | --- Number of notifications currently waiting to be displayed
159 | ---@return integer[]
160 | function notify.pending()
161 |   if not global_instance then
162 |     notify.setup()
163 |   end
164 |   return global_instance.pending()
165 | end
166 | 
167 | function notify._print_history()
168 |   if not global_instance then
169 |     notify.setup()
170 |   end
171 |   for _, notif in ipairs(global_instance.history()) do
172 |     vim.api.nvim_echo({
173 |       {
174 |         vim.fn.strftime(notify._config().time_formats().notification_history, notif.time),
175 |         "NotifyLogTime",
176 |       },
177 |       { " ", "MsgArea" },
178 |       { notif.title[1], "NotifyLogTitle" },
179 |       { #notif.title[1] > 0 and " " or "", "MsgArea" },
180 |       { notif.icon, "Notify" .. notif.level .. "Title" },
181 |       { " ", "MsgArea" },
182 |       { notif.level, "Notify" .. notif.level .. "Title" },
183 |       { " ", "MsgArea" },
184 |       { table.concat(notif.message, "\n"), "MsgArea" },
185 |     }, false, {})
186 |   end
187 | end
188 | 
189 | --- Configure an instance of nvim-notify.
190 | --- You can use this to manage a separate instance of nvim-notify with completely different configuration.
191 | --- The returned instance will have the same functions as the notify module.
192 | ---@param user_config notify.Config
193 | ---@param inherit? boolean Inherit the global configuration, default true
194 | function notify.instance(user_config, inherit)
195 |   return instance(user_config, inherit, global_config)
196 | end
197 | 
198 | setmetatable(notify, {
199 |   __call = function(_, m, l, o)
200 |     if vim.in_fast_event() or vim.fn.has("vim_starting") == 1 then
201 |       vim.schedule(function()
202 |         notify.notify(m, l, o)
203 |       end)
204 |     else
205 |       return notify.notify(m, l, o)
206 |     end
207 |   end,
208 | })
209 | 
210 | return notify
211 | 


--------------------------------------------------------------------------------
/lua/notify/instance.lua:
--------------------------------------------------------------------------------
  1 | local stages = require("notify.stages")
  2 | local config = require("notify.config")
  3 | local Notification = require("notify.service.notification")
  4 | local WindowAnimator = require("notify.windows")
  5 | local NotificationService = require("notify.service")
  6 | local NotificationBuf = require("notify.service.buffer")
  7 | local stage_util = require("notify.stages.util")
  8 | 
  9 | local notif_cmp_keys = {
 10 |   "level",
 11 |   "message",
 12 |   "title",
 13 |   "icon",
 14 | }
 15 | 
 16 | ---@param n1 notify.Notification
 17 | ---@param n2 notify.Notification
 18 | ---@return boolean
 19 | local function notifications_equal(n1, n2)
 20 |   for _, key in ipairs(notif_cmp_keys) do
 21 |     local v1 = n1[key]
 22 |     local v2 = n2[key]
 23 |     -- NOTE: Notification:new adds time string which causes not-equality, so compare only left title (1st element)
 24 |     if key == "title" then
 25 |       v1 = v1[1]
 26 |       v2 = v2[1]
 27 |     end
 28 |     if not vim.deep_equal(v1, v2) then
 29 |       return false
 30 |     end
 31 |   end
 32 |   return true
 33 | end
 34 | 
 35 | ---@param user_config notify.Config
 36 | ---@param inherit? boolean Inherit the global configuration, default true
 37 | ---@param global_config notify.Config
 38 | return function(user_config, inherit, global_config)
 39 |   ---@type notify.Notification[]
 40 |   local notifications = {}
 41 | 
 42 |   user_config = user_config or {}
 43 |   if inherit ~= false and global_config then
 44 |     user_config = vim.tbl_deep_extend("force", global_config, user_config)
 45 |   end
 46 | 
 47 |   local instance_config = config.setup(user_config)
 48 | 
 49 |   local animator_stages = instance_config.stages()
 50 |   local direction = instance_config.top_down() and stage_util.DIRECTION.TOP_DOWN
 51 |     or stage_util.DIRECTION.BOTTOM_UP
 52 | 
 53 |   animator_stages = type(animator_stages) == "string" and stages[animator_stages](direction)
 54 |     or animator_stages
 55 |   local animator = WindowAnimator(animator_stages, instance_config)
 56 |   local service = NotificationService(instance_config, animator)
 57 | 
 58 |   local instance = {}
 59 | 
 60 |   local function get_render(render)
 61 |     if type(render) == "function" then
 62 |       return render
 63 |     end
 64 |     return require("notify.render")[render]
 65 |   end
 66 | 
 67 |   ---@param notif notify.Notification
 68 |   ---@return notify.Notification?
 69 |   local function find_duplicate(notif)
 70 |     for _, buf in pairs(animator.notif_bufs) do
 71 |       if notifications_equal(buf._notif, notif) then
 72 |         return buf._notif
 73 |       end
 74 |     end
 75 |   end
 76 | 
 77 |   function instance.notify(message, level, opts)
 78 |     opts = opts or {}
 79 | 
 80 |     if opts.replace then
 81 |       if type(opts.replace) == "table" then
 82 |         opts.replace = opts.replace.id
 83 |       end
 84 |       local existing = notifications[opts.replace]
 85 |       if not existing then
 86 |         vim.notify("Invalid notification to replace", "error", { title = "nvim-notify" })
 87 |         return
 88 |       end
 89 |       local notif_keys = {
 90 |         "title",
 91 |         "icon",
 92 |         "timeout",
 93 |         "keep",
 94 |         "on_open",
 95 |         "on_close",
 96 |         "render",
 97 |         "hide_from_history",
 98 |         "animate",
 99 |       }
100 |       message = message or existing.message
101 |       level = level or existing.level
102 |       for _, key in ipairs(notif_keys) do
103 |         if opts[key] == nil then
104 |           opts[key] = existing[key]
105 |         end
106 |       end
107 |     end
108 | 
109 |     opts.render = get_render(opts.render or instance_config.render())
110 |     local id = #notifications + 1
111 |     local notification = Notification(id, message, level, opts, instance_config)
112 |     table.insert(notifications, notification)
113 |     local level_num = vim.log.levels[notification.level]
114 | 
115 |     if not opts.replace and instance_config.merge_duplicates() then
116 |       local dup = find_duplicate(notification)
117 |       if dup then
118 |         dup.duplicates = dup.duplicates or { dup.id }
119 |         table.insert(dup.duplicates, notification.id)
120 |         notification.duplicates = dup.duplicates
121 | 
122 |         local min_dups = instance_config.merge_duplicates()
123 |         if min_dups == true or #notification.duplicates >= min_dups + 1 then
124 |           opts.replace = dup.id
125 |         end
126 |       end
127 |     end
128 | 
129 |     if opts.replace then
130 |       service:replace(opts.replace, notification)
131 |     elseif not level_num or level_num >= instance_config.level() then
132 |       service:push(notification)
133 |     end
134 |     return {
135 |       id = id,
136 |     }
137 |   end
138 | 
139 |   ---@param notif_id integer|notify.Record
140 |   ---@param opts table
141 |   function instance.open(notif_id, opts)
142 |     opts = opts or {}
143 |     if type(notif_id) == "table" then
144 |       notif_id = notif_id.id
145 |     end
146 |     local notif = notifications[notif_id]
147 |     if not notif then
148 |       vim.notify(
149 |         "Invalid notification id: " .. notif_id,
150 |         vim.log.levels.WARN,
151 |         { title = "nvim-notify" }
152 |       )
153 |       return
154 |     end
155 |     local buf = opts.buffer or vim.api.nvim_create_buf(false, true)
156 |     local notif_buf =
157 |       NotificationBuf(buf, notif, vim.tbl_extend("keep", opts, { config = instance_config }))
158 |     notif_buf:render()
159 |     return {
160 |       buffer = buf,
161 |       height = notif_buf:height(),
162 |       width = notif_buf:width(),
163 |       highlights = {
164 |         body = notif_buf.highlights.body,
165 |         border = notif_buf.highlights.border,
166 |         title = notif_buf.highlights.title,
167 |         icon = notif_buf.highlights.icon,
168 |       },
169 |     }
170 |   end
171 | 
172 |   function instance.async(message, level, opts)
173 |     opts = opts or {}
174 |     local async = require("plenary.async")
175 |     local send_close, wait_close = async.control.channel.oneshot()
176 |     opts.on_close = send_close
177 | 
178 |     local send_open, wait_open = async.control.channel.oneshot()
179 |     opts.on_open = send_open
180 | 
181 |     async.util.scheduler()
182 |     local record = instance.notify(message, level, opts)
183 |     return vim.tbl_extend("error", record, {
184 |       events = {
185 |         open = wait_open,
186 |         close = wait_close,
187 |       },
188 |     })
189 |   end
190 | 
191 |   function instance.history(args)
192 |     args = args or {}
193 |     local records = {}
194 |     for _, notif in ipairs(notifications) do
195 |       if not notif.hide_from_history or args.include_hidden then
196 |         records[#records + 1] = notif:record()
197 |       end
198 |     end
199 |     return records
200 |   end
201 | 
202 |   function instance.dismiss(opts)
203 |     if service then
204 |       service:dismiss(opts or {})
205 |     end
206 |   end
207 | 
208 |   function instance.pending()
209 |     return service and service:pending() or {}
210 |   end
211 | 
212 |   function instance.clear_history()
213 |     notifications = {}
214 |   end
215 | 
216 |   setmetatable(instance, {
217 |     __call = function(_, m, l, o)
218 |       if vim.in_fast_event() then
219 |         vim.schedule(function()
220 |           instance.notify(m, l, o)
221 |         end)
222 |       else
223 |         return instance.notify(m, l, o)
224 |       end
225 |     end,
226 |   })
227 |   return instance, instance_config.merged()
228 | end
229 | 


--------------------------------------------------------------------------------
/lua/notify/integrations/fzf.lua:
--------------------------------------------------------------------------------
  1 | local notify = require("notify")
  2 | local time_format = notify._config().time_formats().notification
  3 | 
  4 | local builtin = require("fzf-lua.previewer.builtin")
  5 | local fzf = require("fzf-lua")
  6 | 
  7 | local M = {}
  8 | 
  9 | ---@alias NotifyMessage {id: number, message: notify.Record, texts: string[][]}
 10 | ---@alias NotifyEntry {ordinal: string, display: string}
 11 | 
 12 | ---@param message NotifyMessage
 13 | ---@return NotifyEntry
 14 | function M.entry(message)
 15 |   local display = message.id .. " " ---@type string
 16 |   local content = ""
 17 |   for _, text in ipairs(message.texts) do
 18 |     ---@type string?
 19 |     local hl_group = text[2]
 20 |     display = display .. (hl_group and fzf.utils.ansi_from_hl(hl_group, text[1]) or text[1])
 21 |     content = content .. text[1]
 22 |   end
 23 | 
 24 |   return {
 25 |     message = message.message,
 26 |     ordinal = content,
 27 |     display = display,
 28 |   }
 29 | end
 30 | 
 31 | function M.find()
 32 |   local messages = notify.history()
 33 | 
 34 |   ---@type table<number, NotifyEntry>
 35 |   local ret = {}
 36 | 
 37 |   for _, message in ipairs(messages) do
 38 |     ret[message.id] = M.entry({
 39 |       id = message.id,
 40 |       message = message,
 41 |       texts = {
 42 |         { vim.fn.strftime(time_format, message.time) .. " ", "NotifyLogTime" },
 43 |         { message.title[1] .. " ", "NotifyLogTitle" },
 44 |         { message.icon .. " ", "Notify" .. message.level .. "Title" },
 45 |         { message.level .. " ", "Notify" .. message.level .. "Title" },
 46 |         { message.message[1], "Notify" .. message.level .. "Body" },
 47 |       },
 48 |     })
 49 |   end
 50 | 
 51 |   return ret
 52 | end
 53 | 
 54 | function M.parse_entry(messages, entry_str)
 55 |   local id = tonumber(entry_str:match("^%d+"))
 56 |   local entry = messages[id]
 57 |   return entry
 58 | end
 59 | 
 60 | ---@param messages table<number, NotifyEntry>
 61 | function M.previewer(messages)
 62 |   local previewer = builtin.buffer_or_file:extend()
 63 | 
 64 |   function previewer:new(o, opts, fzf_win)
 65 |     previewer.super.new(self, o, opts, fzf_win)
 66 |     self.title = "Message"
 67 |     setmetatable(self, previewer)
 68 |     return self
 69 |   end
 70 | 
 71 |   function previewer:populate_preview_buf(entry_str)
 72 |     local buf = self:get_tmp_buffer()
 73 |     local entry = M.parse_entry(messages, entry_str)
 74 | 
 75 |     if entry then
 76 |       local notification = entry.message
 77 |       notify.open(notification, { buffer = buf, max_width = 0 })
 78 |     end
 79 | 
 80 |     self:set_preview_buf(buf)
 81 |     self.win:update_preview_title(" Message ")
 82 |     self.win:update_preview_scrollbar()
 83 |     self.win:set_winopts(self.win.preview_winid, { wrap = true })
 84 |   end
 85 | 
 86 |   return previewer
 87 | end
 88 | 
 89 | ---@param opts? table<string, any>
 90 | function M.open(opts)
 91 |   local messages = M.find()
 92 |   opts = vim.tbl_deep_extend("force", opts or {}, {
 93 |     prompt = false,
 94 |     winopts = {
 95 |       title = " Filter Notifications ",
 96 |       title_pos = "center",
 97 |       preview = {
 98 |         title = " Message ",
 99 |         title_pos = "center",
100 |       },
101 |     },
102 |     previewer = M.previewer(messages),
103 |     fzf_opts = {
104 |       ["--no-multi"] = "",
105 |       ["--with-nth"] = "2..",
106 |     },
107 |     actions = {
108 |       default = function(selected)
109 |         if #selected == 0 then
110 |           return
111 |         end
112 |         local notification = M.parse_entry(messages, selected[1]).message
113 | 
114 |         local opened_buffer = notify.open(notification)
115 | 
116 |         local lines = vim.opt.lines:get()
117 |         local cols = vim.opt.columns:get()
118 | 
119 |         local win = vim.api.nvim_open_win(opened_buffer.buffer, true, {
120 |           relative = "editor",
121 |           row = (lines - opened_buffer.height) / 2,
122 |           col = (cols - opened_buffer.width) / 2,
123 |           height = opened_buffer.height,
124 |           width = opened_buffer.width,
125 |           border = "rounded",
126 |           style = "minimal",
127 |         })
128 |         -- vim.wo does not behave like setlocal, thus we use setwinvar to set local
129 |         -- only options. Otherwise our changes would affect subsequently opened
130 |         -- windows.
131 |         -- see e.g. neovim#14595
132 |         vim.fn.setwinvar(
133 |           win,
134 |           "&winhl",
135 |           "Normal:"
136 |             .. opened_buffer.highlights.body
137 |             .. ",FloatBorder:"
138 |             .. opened_buffer.highlights.border
139 |         )
140 |         vim.fn.setwinvar(win, "&wrap", 0)
141 |       end,
142 |     },
143 |   })
144 |   local lines = vim.tbl_map(function(entry)
145 |     return entry.display
146 |   end, vim.tbl_values(messages))
147 |   return fzf.fzf_exec(lines, opts)
148 | end
149 | 
150 | return M
151 | 


--------------------------------------------------------------------------------
/lua/notify/integrations/init.lua:
--------------------------------------------------------------------------------
 1 | local M = {}
 2 | 
 3 | function M.pick()
 4 |   if pcall(_G.require, "telescope.config") then
 5 |     require("telescope").extensions.notify.noitfy({})
 6 |   elseif pcall(_G.require, "fzf-lua") then
 7 |     require("notify.integrations.fzf").open({})
 8 |   else
 9 |     error("No picker available")
10 |   end
11 | end
12 | return M
13 | 


--------------------------------------------------------------------------------
/lua/notify/render/base.lua:
--------------------------------------------------------------------------------
 1 | local M = {}
 2 | 
 3 | local namespace = vim.api.nvim_create_namespace("nvim-notify")
 4 | 
 5 | function M.namespace()
 6 |   return namespace
 7 | end
 8 | 
 9 | return M
10 | 


--------------------------------------------------------------------------------
/lua/notify/render/compact.lua:
--------------------------------------------------------------------------------
 1 | local base = require("notify.render.base")
 2 | 
 3 | return function(bufnr, notif, highlights)
 4 |   local namespace = base.namespace()
 5 |   local icon = notif.icon
 6 |   local title = notif.title[1]
 7 | 
 8 |   if type(title) == "string" and notif.duplicates then
 9 |     title = string.format("%s x%d", title, #notif.duplicates)
10 |   end
11 | 
12 |   local prefix
13 |   if type(title) == "string" and #title > 0 then
14 |     prefix = string.format("%s | %s:", icon, title)
15 |   else
16 |     prefix = string.format("%s |", icon)
17 |   end
18 |   local message = {
19 |     string.format("%s %s", prefix, notif.message[1]),
20 |     unpack(notif.message, 2),
21 |   }
22 | 
23 |   vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, message)
24 | 
25 |   local icon_length = string.len(icon)
26 |   local prefix_length = string.len(prefix)
27 | 
28 |   vim.api.nvim_buf_set_extmark(bufnr, namespace, 0, 0, {
29 |     hl_group = highlights.icon,
30 |     end_col = icon_length + 1,
31 |     priority = 50,
32 |   })
33 |   vim.api.nvim_buf_set_extmark(bufnr, namespace, 0, icon_length + 1, {
34 |     hl_group = highlights.title,
35 |     end_col = prefix_length + 1,
36 |     priority = 50,
37 |   })
38 |   vim.api.nvim_buf_set_extmark(bufnr, namespace, 0, prefix_length + 1, {
39 |     hl_group = highlights.body,
40 |     end_line = #message,
41 |     priority = 50,
42 |   })
43 | end
44 | 


--------------------------------------------------------------------------------
/lua/notify/render/default.lua:
--------------------------------------------------------------------------------
 1 | local api = vim.api
 2 | local base = require("notify.render.base")
 3 | local util = require("notify.util")
 4 | 
 5 | return function(bufnr, notif, highlights, config)
 6 |   local left_icon = notif.icon == "" and "" or notif.icon .. " "
 7 |   local max_message_width = util.max_line_width(notif.message)
 8 | 
 9 |   local right_title = notif.title[2]
10 |   local left_title = notif.title[1]
11 |   if notif.duplicates then
12 |     left_title = string.format("%s (x%d)", left_title, #notif.duplicates)
13 |   end
14 |   local title_accum = vim.api.nvim_strwidth(left_icon)
15 |     + vim.api.nvim_strwidth(right_title)
16 |     + vim.api.nvim_strwidth(left_title)
17 | 
18 |   local left_buffer = string.rep(" ", math.max(0, max_message_width - title_accum))
19 | 
20 |   local namespace = base.namespace()
21 |   api.nvim_buf_set_lines(bufnr, 0, 1, false, { "", "" })
22 | 
23 |   local virt_text = left_icon == "" and {} or { { " " }, { left_icon, highlights.icon } }
24 |   table.insert(virt_text, { left_title .. left_buffer, highlights.title })
25 |   api.nvim_buf_set_extmark(bufnr, namespace, 0, 0, {
26 |     virt_text = virt_text,
27 |     virt_text_win_col = 0,
28 |     priority = 10,
29 |   })
30 |   api.nvim_buf_set_extmark(bufnr, namespace, 0, 0, {
31 |     virt_text = { { " " }, { right_title, highlights.title }, { " " } },
32 |     virt_text_pos = "right_align",
33 |     priority = 10,
34 |   })
35 |   api.nvim_buf_set_extmark(bufnr, namespace, 1, 0, {
36 |     virt_text = {
37 |       {
38 |         string.rep(
39 |           "━",
40 |           math.max(vim.api.nvim_strwidth(left_buffer) + title_accum + 2, config.minimum_width())
41 |         ),
42 |         highlights.border,
43 |       },
44 |     },
45 |     virt_text_win_col = 0,
46 |     priority = 10,
47 |   })
48 |   api.nvim_buf_set_lines(bufnr, 2, -1, false, notif.message)
49 | 
50 |   api.nvim_buf_set_extmark(bufnr, namespace, 2, 0, {
51 |     hl_group = highlights.body,
52 |     end_line = 1 + #notif.message,
53 |     end_col = #notif.message[#notif.message],
54 |     priority = 50, -- Allow treesitter to override
55 |   })
56 | end
57 | 


--------------------------------------------------------------------------------
/lua/notify/render/init.lua:
--------------------------------------------------------------------------------
 1 | ---@tag notify-render
 2 | ---@text
 3 | --- Notification buffer rendering
 4 | ---
 5 | --- Custom rendering can be provided by both the user config in the setup or on
 6 | --- an individual notification using the `render` key.
 7 | --- The key can either be the name of a built-in renderer or a custom function.
 8 | ---
 9 | --- Built-in renderers:
10 | --- - `"default"`
11 | --- - `"minimal"`
12 | --- - `"simple"`
13 | --- - `"compact"`
14 | --- - `"wrapped-compact"`
15 | --- - `"wrapped-default"`
16 | ---
17 | --- Custom functions should accept a buffer, a notification record and a highlights table
18 | ---
19 | --- >
20 | ---     render: fun(buf: integer, notification: notify.Record, highlights: notify.Highlights, config)
21 | --- <
22 | --- You should use the provided highlight groups to take advantage of opacity
23 | --- changes as they will be updated as the notification is animated
24 | 
25 | ---@class notify.Highlights
26 | ---@field title string
27 | ---@field icon string
28 | ---@field border string
29 | ---@field body string
30 | 
31 | local M = {}
32 | 
33 | setmetatable(M, {
34 |   __index = function(_, key)
35 |     return require("notify.render." .. key)
36 |   end,
37 | })
38 | 
39 | return M
40 | 


--------------------------------------------------------------------------------
/lua/notify/render/minimal.lua:
--------------------------------------------------------------------------------
 1 | local api = vim.api
 2 | local base = require("notify.render.base")
 3 | 
 4 | return function(bufnr, notif, highlights)
 5 |   local message = notif.message
 6 |   if notif.duplicates then
 7 |     message = {
 8 |       string.format("x%d %s", #notif.duplicates, notif.message[1]),
 9 |       unpack(notif.message, 2),
10 |     }
11 |   end
12 | 
13 |   local namespace = base.namespace()
14 |   api.nvim_buf_set_lines(bufnr, 0, -1, false, message)
15 | 
16 |   api.nvim_buf_set_extmark(bufnr, namespace, 0, 0, {
17 |     hl_group = highlights.icon,
18 |     end_line = #message - 1,
19 |     end_col = #message[#message],
20 |     priority = 50,
21 |   })
22 | end
23 | 


--------------------------------------------------------------------------------
/lua/notify/render/simple.lua:
--------------------------------------------------------------------------------
 1 | local api = vim.api
 2 | local base = require("notify.render.base")
 3 | local util = require("notify.util")
 4 | 
 5 | return function(bufnr, notif, highlights, config)
 6 |   local max_message_width = util.max_line_width(notif.message)
 7 | 
 8 |   local title = notif.title[1]
 9 |   if notif.duplicates then
10 |     title = string.format("%s (x%d)", title, #notif.duplicates)
11 |   end
12 |   local title_accum = vim.api.nvim_strwidth(title)
13 | 
14 |   local title_buffer = string.rep(
15 |     " ",
16 |     (math.max(max_message_width, title_accum, config.minimum_width()) - title_accum) / 2
17 |   )
18 | 
19 |   local namespace = base.namespace()
20 | 
21 |   api.nvim_buf_set_lines(bufnr, 0, 1, false, { "", "" })
22 |   api.nvim_buf_set_extmark(bufnr, namespace, 0, 0, {
23 |     virt_text = {
24 |       { title_buffer .. title .. title_buffer, highlights.title },
25 |     },
26 |     virt_text_win_col = 0,
27 |     priority = 10,
28 |   })
29 |   api.nvim_buf_set_extmark(bufnr, namespace, 1, 0, {
30 |     virt_text = {
31 |       {
32 |         string.rep("━", math.max(max_message_width, title_accum, config.minimum_width())),
33 |         highlights.border,
34 |       },
35 |     },
36 |     virt_text_win_col = 0,
37 |     priority = 10,
38 |   })
39 |   api.nvim_buf_set_lines(bufnr, 2, -1, false, notif.message)
40 | 
41 |   api.nvim_buf_set_extmark(bufnr, namespace, 2, 0, {
42 |     hl_group = highlights.body,
43 |     end_line = 1 + #notif.message,
44 |     end_col = #notif.message[#notif.message],
45 |     priority = 50,
46 |   })
47 | end
48 | 


--------------------------------------------------------------------------------
/lua/notify/render/wrapped-compact.lua:
--------------------------------------------------------------------------------
 1 | -- WRAPS TEXT AND ADDS SOME PADDING.
 2 | --------------------------------------------------------------------------------
 3 | 
 4 | ---@param line string
 5 | ---@param width number
 6 | ---@return string[]
 7 | local function split_length(line, width)
 8 |   local text = {}
 9 |   local next_line
10 |   while true do
11 |     if #line == 0 then
12 |       return text
13 |     end
14 |     next_line, line = line:sub(1, width), line:sub(width + 1)
15 |     text[#text + 1] = next_line
16 |   end
17 | end
18 | 
19 | ---@param lines string[]
20 | ---@param max_width number
21 | ---@return string[]
22 | local function custom_wrap(lines, max_width)
23 |   local right_pad = "  "
24 |   local wrapped_lines = {}
25 |   for _, line in pairs(lines) do
26 |     local new_lines = split_length(line, max_width - #right_pad)
27 |     for _, nl in ipairs(new_lines) do
28 |       table.insert(wrapped_lines, nl:gsub("^%s+", "") .. right_pad)
29 |     end
30 |   end
31 |   return wrapped_lines
32 | end
33 | 
34 | ---@param bufnr number
35 | ---@param notif object
36 | ---@param highlights object
37 | ---@param config object plugin config_obj
38 | return function(bufnr, notif, highlights, config)
39 |   local namespace = require("notify.render.base").namespace()
40 |   local icon = notif.icon
41 |   local icon_length = #icon
42 |   local prefix = ""
43 |   local prefix_length = 0
44 |   local message = custom_wrap(notif.message, config.max_width() or 80)
45 |   local title = notif.title[1]
46 |   local default_titles = { "Error", "Warning", "Notify" }
47 |   local has_valid_manual_title = type(title) == "string"
48 |     and #title > 0
49 |     and not vim.tbl_contains(default_titles, title)
50 | 
51 |   if has_valid_manual_title then
52 |     prefix = string.format("%s %s ", icon, title)
53 |     if notif.duplicates then
54 |       prefix = string.format("%s x%d", prefix, #notif.duplicates)
55 |     end
56 |     prefix_length = #prefix
57 |     table.insert(message, 1, prefix)
58 |   end
59 | 
60 |   message[1] = " " .. message[1]
61 |   vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, message)
62 | 
63 |   if has_valid_manual_title then
64 |     vim.api.nvim_buf_set_extmark(bufnr, namespace, 0, 0, {
65 |       hl_group = highlights.icon,
66 |       end_col = icon_length + 1,
67 |       priority = 50,
68 |     })
69 |     vim.api.nvim_buf_set_extmark(bufnr, namespace, 0, icon_length + 1, {
70 |       hl_group = highlights.title,
71 |       end_col = prefix_length + 1,
72 |       priority = 50,
73 |     })
74 |   end
75 | 
76 |   vim.api.nvim_buf_set_extmark(bufnr, namespace, 0, prefix_length + 1, {
77 |     hl_group = highlights.body,
78 |     end_line = #message,
79 |     priority = 50,
80 |   })
81 | 
82 |   -- padding to the left/right
83 |   for ln = 1, #message do
84 |     vim.api.nvim_buf_set_extmark(bufnr, namespace, ln, 0, {
85 |       virt_text = { { " ", highlights.body } },
86 |       virt_text_pos = "inline",
87 |     })
88 |     vim.api.nvim_buf_set_extmark(bufnr, namespace, ln, 0, {
89 |       virt_text = { { " ", highlights.body } },
90 |       virt_text_pos = "right_align",
91 |     })
92 |   end
93 | end
94 | 


--------------------------------------------------------------------------------
/lua/notify/render/wrapped-default.lua:
--------------------------------------------------------------------------------
 1 | local vim_api = vim.api
 2 | local base = require("notify.render.base")
 3 | 
 4 | ---@param line string
 5 | ---@param width number
 6 | ---@return table
 7 | local function split_length(line, width)
 8 |   local text = {}
 9 |   local next_line
10 |   while true do
11 |     if #line == 0 then
12 |       return text
13 |     end
14 |     next_line, line = line:sub(1, width), line:sub(width + 1)
15 |     text[#text + 1] = next_line
16 |   end
17 | end
18 | 
19 | ---@param lines string[]
20 | ---@param max_width number
21 | ---@return table
22 | local function custom_wrap(lines, max_width)
23 |   local wrapped_lines = {}
24 |   for _, line in pairs(lines) do
25 |     local new_lines = split_length(line, max_width)
26 |     for _, nl in ipairs(new_lines) do
27 |       nl = nl:gsub("^%s*", " "):gsub("%s*
quot;, " ")
28 |       table.insert(wrapped_lines, nl)
29 |     end
30 |   end
31 |   return wrapped_lines
32 | end
33 | 
34 | ---@param bufnr number
35 | ---@param notif notify.Record
36 | ---@param highlights notify.Highlights
37 | ---@param config notify.Config
38 | return function(bufnr, notif, highlights, config)
39 |   local namespace = base.namespace()
40 |   local icon = notif.icon .. " "
41 |   local title = notif.title[1] or "Notify"
42 | 
43 |   local terminal_width = vim.o.columns
44 |   local default_max_width = math.floor((terminal_width * 30) / 100)
45 |   local max_width = config.max_width and config.max_width() or default_max_width
46 | 
47 |   -- Ensure max_width is within bounds
48 |   max_width = math.max(10, math.min(max_width, terminal_width - 1))
49 | 
50 |   local message = custom_wrap(notif.message, max_width)
51 | 
52 |   local prefix = string.format(" %s %s", icon, title)
53 |   table.insert(message, 1, prefix)
54 |   table.insert(message, 2, string.rep("━", max_width))
55 | 
56 |   vim_api.nvim_buf_set_lines(bufnr, 0, -1, false, message)
57 | 
58 |   local prefix_length = vim.str_utfindex(prefix)
59 |   prefix_length = math.min(prefix_length, max_width - 1)
60 | 
61 |   vim_api.nvim_buf_set_extmark(bufnr, namespace, 0, 0, {
62 |     virt_text = {
63 |       { " " },
64 |       { icon, highlights.icon },
65 |       { title, highlights.title },
66 |       { " " },
67 |     },
68 |     virt_text_win_col = 0,
69 |     priority = 10,
70 |   })
71 | 
72 |   vim_api.nvim_buf_set_extmark(bufnr, namespace, 1, 0, {
73 |     virt_text = {
74 |       { string.rep("━", max_width), highlights.border },
75 |     },
76 |     virt_text_win_col = 0,
77 |     priority = 10,
78 |   })
79 | 
80 |   vim_api.nvim_buf_set_extmark(bufnr, namespace, 2, 0, {
81 |     hl_group = highlights.body,
82 |     end_line = #message,
83 |     end_col = 0,
84 |     priority = 50,
85 |   })
86 | end
87 | 


--------------------------------------------------------------------------------
/lua/notify/service/buffer/highlights.lua:
--------------------------------------------------------------------------------
  1 | local util = require("notify.util")
  2 | 
  3 | ---@class NotifyBufHighlights
  4 | ---@field groups table
  5 | ---@field opacity number
  6 | ---@field title string
  7 | ---@field border string
  8 | ---@field icon string
  9 | ---@field body string
 10 | ---@field buffer number
 11 | ---@field _config table
 12 | local NotifyBufHighlights = {}
 13 | 
 14 | function NotifyBufHighlights:new(level, buffer, config)
 15 |   local function linked_group(section)
 16 |     local orig = "Notify" .. level .. section
 17 |     if vim.fn.hlID(orig) == 0 then
 18 |       orig = "NotifyINFO" .. section
 19 |     end
 20 |     local new = orig .. buffer
 21 | 
 22 |     vim.api.nvim_set_hl(0, new, { link = orig })
 23 |     local hl = vim.api.nvim_get_hl(0, { name = orig, create = false, link = false })
 24 |     -- Removes the unwanted 'default' key, as we will copy the table for updating the highlight later.
 25 |     hl.default = nil
 26 | 
 27 |     return new, hl
 28 |   end
 29 | 
 30 |   local title, title_def = linked_group("Title")
 31 |   local border, border_def = linked_group("Border")
 32 |   local body, body_def = linked_group("Body")
 33 |   local icon, icon_def = linked_group("Icon")
 34 | 
 35 |   local groups = {
 36 |     [title] = title_def,
 37 |     [border] = border_def,
 38 |     [body] = body_def,
 39 |     [icon] = icon_def,
 40 |   }
 41 |   local buf_highlights = {
 42 |     groups = groups,
 43 |     opacity = 100,
 44 |     border = border,
 45 |     body = body,
 46 |     title = title,
 47 |     icon = icon,
 48 |     buffer = buffer,
 49 |     _config = config,
 50 |   }
 51 |   self.__index = self
 52 |   setmetatable(buf_highlights, self)
 53 |   return buf_highlights
 54 | end
 55 | 
 56 | function NotifyBufHighlights:_redefine_treesitter()
 57 |   local buf_highlighter = require("vim.treesitter.highlighter").active[self.buffer]
 58 | 
 59 |   if not buf_highlighter then
 60 |     return
 61 |   end
 62 |   local render_namespace = vim.api.nvim_create_namespace("notify-treesitter-override")
 63 |   vim.api.nvim_buf_clear_namespace(self.buffer, render_namespace, 0, -1)
 64 | 
 65 |   local function link(orig)
 66 |     local new = orig .. self.buffer
 67 |     if self.groups[new] then
 68 |       return new
 69 |     end
 70 |     vim.api.nvim_set_hl(0, new, { link = orig })
 71 |     self.groups[new] = vim.api.nvim_get_hl(0, { name = new, link = false })
 72 |     return new
 73 |   end
 74 | 
 75 |   local matches = {}
 76 | 
 77 |   local i = 0
 78 |   buf_highlighter.tree:for_each_tree(function(tstree, tree)
 79 |     if not tstree then
 80 |       return
 81 |     end
 82 | 
 83 |     local root = tstree:root()
 84 | 
 85 |     local query = buf_highlighter:get_query(tree:lang())
 86 | 
 87 |     -- Some injected languages may not have highlight queries.
 88 |     if not query:query() then
 89 |       return
 90 |     end
 91 | 
 92 |     local iter = query:query():iter_captures(root, buf_highlighter.bufnr)
 93 | 
 94 |     for capture, node, metadata in iter do
 95 |       -- Wait until we get at least a single capture as we don't know when parsing is complete.
 96 |       self._treesitter_redefined = true
 97 |       local hl = query.hl_cache[capture]
 98 | 
 99 |       if hl then
100 |         i = i + 1
101 |         local c = query._query.captures[capture] -- name of the capture in the query
102 |         if c ~= nil then
103 |           local capture_hl
104 |           -- Removed in nightly with change of highlight names to @...
105 |           -- https://github.com/neovim/neovim/pull/19931
106 |           if query._get_hl_from_capture then
107 |             local general_hl, is_vim_hl = query:_get_hl_from_capture(capture)
108 |             capture_hl = is_vim_hl and general_hl or (tree:lang() .. general_hl)
109 |           else
110 |             capture_hl = query._query.captures[capture]
111 |             if not vim.startswith(capture_hl, "_") then
112 |               capture_hl = "@" .. capture_hl .. "." .. tree:lang()
113 |             end
114 |           end
115 | 
116 |           local start_row, start_col, end_row, end_col = node:range()
117 |           local custom_hl = link(capture_hl)
118 | 
119 |           vim.api.nvim_buf_set_extmark(self.buffer, render_namespace, start_row, start_col, {
120 |             end_row = end_row,
121 |             end_col = end_col,
122 |             hl_group = custom_hl,
123 |             -- TODO: Not sure how neovim's highlighter doesn't have issues with overriding highlights
124 |             -- Three marks on same region always show the second for some reason AFAICT
125 |             priority = tonumber(metadata.priority) or i + 200,
126 |             conceal = metadata.conceal,
127 |           })
128 |         end
129 |       end
130 |     end
131 |   end, true)
132 |   return matches
133 | end
134 | 
135 | function NotifyBufHighlights:set_opacity(alpha)
136 |   if
137 |     not self._treesitter_redefined
138 |     and vim.api.nvim_get_option_value("filetype", { buf = self.buffer }) ~= "notify"
139 |   then
140 |     self:_redefine_treesitter()
141 |   end
142 |   self.opacity = alpha
143 |   local background = self._config.background_colour()
144 |   local updated = false
145 |   for group, fields in pairs(self.groups) do
146 |     local fg = fields.fg
147 |     if fg then
148 |       fg = util.blend(fg, background, alpha / 100)
149 |     end
150 |     local bg = fields.bg
151 |     if bg then
152 |       bg = util.blend(bg, background, alpha / 100)
153 |     end
154 | 
155 |     if fg ~= fields.fg or bg ~= fields.bg then
156 |       local hl = vim.tbl_extend("force", fields, { fg = fg, bg = bg })
157 |       vim.api.nvim_set_hl(0, group, hl)
158 |       updated = true
159 |     end
160 |   end
161 |   return updated
162 | end
163 | 
164 | function NotifyBufHighlights:get_opacity()
165 |   return self.opacity
166 | end
167 | 
168 | ---@return NotifyBufHighlights
169 | return function(level, buffer, config)
170 |   return NotifyBufHighlights:new(level, buffer, config)
171 | end
172 | 


--------------------------------------------------------------------------------
/lua/notify/service/buffer/init.lua:
--------------------------------------------------------------------------------
  1 | local api = vim.api
  2 | 
  3 | local NotifyBufHighlights = require("notify.service.buffer.highlights")
  4 | 
  5 | ---@class NotificationBuf
  6 | ---@field highlights NotifyBufHighlights
  7 | ---@field _config table
  8 | ---@field _notif notify.Notification
  9 | ---@field _state "open" | "closed"
 10 | ---@field _buffer number
 11 | ---@field _height number
 12 | ---@field _width number
 13 | ---@field _max_width number | nil
 14 | local NotificationBuf = {}
 15 | 
 16 | local BufState = {
 17 |   OPEN = "open",
 18 |   CLOSED = "close",
 19 | }
 20 | 
 21 | function NotificationBuf:new(kwargs)
 22 |   local notif_buf = {
 23 |     _config = kwargs.config,
 24 |     _max_width = kwargs.max_width,
 25 |     _buffer = kwargs.buffer,
 26 |     _state = BufState.CLOSED,
 27 |     _width = 0,
 28 |     _height = 0,
 29 |   }
 30 |   setmetatable(notif_buf, self)
 31 |   self.__index = self
 32 |   notif_buf:set_notification(kwargs.notif)
 33 |   return notif_buf
 34 | end
 35 | 
 36 | function NotificationBuf:set_notification(notif)
 37 |   self._notif = notif
 38 |   self:_create_highlights()
 39 | end
 40 | 
 41 | function NotificationBuf:_create_highlights()
 42 |   local existing_opacity = self.highlights and self.highlights.opacity or 100
 43 |   self.highlights = NotifyBufHighlights(self._notif.level, self._buffer, self._config)
 44 |   if existing_opacity < 100 then
 45 |     self.highlights:set_opacity(existing_opacity)
 46 |   end
 47 | end
 48 | 
 49 | function NotificationBuf:open(win)
 50 |   if self._state ~= BufState.CLOSED then
 51 |     return
 52 |   end
 53 |   self._state = BufState.OPEN
 54 |   local record = self._notif:record()
 55 |   if self._notif.on_open then
 56 |     self._notif.on_open(win, record)
 57 |   end
 58 |   if self._config.on_open() then
 59 |     self._config.on_open()(win, record)
 60 |   end
 61 | end
 62 | 
 63 | function NotificationBuf:should_animate()
 64 |   return self._notif.animate
 65 | end
 66 | 
 67 | function NotificationBuf:close(win)
 68 |   if self._state ~= BufState.OPEN then
 69 |     return
 70 |   end
 71 |   self._state = BufState.CLOSED
 72 |   vim.schedule(function()
 73 |     if self._notif.on_close then
 74 |       self._notif.on_close(win)
 75 |     end
 76 |     if self._config.on_close() then
 77 |       self._config.on_close()(win)
 78 |     end
 79 |     pcall(api.nvim_buf_delete, self._buffer, { force = true })
 80 |   end)
 81 | end
 82 | 
 83 | function NotificationBuf:height()
 84 |   return self._height
 85 | end
 86 | 
 87 | function NotificationBuf:width()
 88 |   return self._width
 89 | end
 90 | 
 91 | function NotificationBuf:should_stay()
 92 |   if self._notif.keep then
 93 |     return self._notif.keep()
 94 |   end
 95 |   return false
 96 | end
 97 | 
 98 | function NotificationBuf:render()
 99 |   local notif = self._notif
100 |   local buf = self._buffer
101 | 
102 |   local render_namespace = require("notify.render.base").namespace()
103 |   api.nvim_buf_set_option(buf, "filetype", "notify")
104 |   api.nvim_buf_set_option(buf, "modifiable", true)
105 |   api.nvim_buf_clear_namespace(buf, render_namespace, 0, -1)
106 | 
107 |   notif.render(buf, notif, self.highlights, self._config)
108 | 
109 |   api.nvim_buf_set_option(buf, "modifiable", false)
110 | 
111 |   local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
112 |   local width = self._config.minimum_width()
113 |   for _, line in pairs(lines) do
114 |     width = math.max(width, vim.api.nvim_strwidth(line))
115 |   end
116 |   local success, extmarks =
117 |     pcall(api.nvim_buf_get_extmarks, buf, render_namespace, 0, #lines, { details = true })
118 |   if not success then
119 |     extmarks = {}
120 |   end
121 |   local virt_texts = {}
122 |   for _, mark in ipairs(extmarks) do
123 |     local details = mark[4]
124 |     for _, virt_text in ipairs(details.virt_text or {}) do
125 |       virt_texts[mark[2]] = (virt_texts[mark[2]] or "") .. virt_text[1]
126 |     end
127 |   end
128 |   for _, text in pairs(virt_texts) do
129 |     width = math.max(width, vim.api.nvim_strwidth(text))
130 |   end
131 | 
132 |   self._width = width
133 |   self._height = #lines
134 | end
135 | 
136 | function NotificationBuf:timeout()
137 |   return self._notif.timeout
138 | end
139 | 
140 | function NotificationBuf:buffer()
141 |   return self._buffer
142 | end
143 | 
144 | function NotificationBuf:is_valid()
145 |   return self._buffer and vim.api.nvim_buf_is_valid(self._buffer)
146 | end
147 | 
148 | function NotificationBuf:level()
149 |   return self._notif.level
150 | end
151 | 
152 | ---@param buf number
153 | ---@param notification notify.Notification;q
154 | ---@return NotificationBuf
155 | return function(buf, notification, opts)
156 |   return NotificationBuf:new(
157 |     vim.tbl_extend("keep", { buffer = buf, notif = notification }, opts or {})
158 |   )
159 | end
160 | 


--------------------------------------------------------------------------------
/lua/notify/service/init.lua:
--------------------------------------------------------------------------------
  1 | local util = require("notify.util")
  2 | local NotificationBuf = require("notify.service.buffer")
  3 | 
  4 | ---@class NotificationService
  5 | ---@field private _running boolean
  6 | ---@field private _pending FIFOQueue
  7 | ---@field private _animator WindowAnimator
  8 | ---@field private _buffers table<integer, NotificationBuf>
  9 | ---@field private _fps integer
 10 | local NotificationService = {}
 11 | 
 12 | ---@class notify.ServiceConfig
 13 | ---@field fps integer
 14 | 
 15 | ---@param config notify.ServiceConfig
 16 | function NotificationService:new(config, animator)
 17 |   local service = {
 18 |     _config = config,
 19 |     _fps = config.fps(),
 20 |     _animator = animator,
 21 |     _pending = util.FIFOQueue(),
 22 |     _running = false,
 23 |     _buffers = {},
 24 |   }
 25 |   self.__index = self
 26 |   setmetatable(service, self)
 27 |   return service
 28 | end
 29 | 
 30 | function NotificationService:_run()
 31 |   self._running = true
 32 |   local succees, updated =
 33 |     pcall(self._animator.render, self._animator, self._pending, 1 / self._fps)
 34 |   if not succees then
 35 |     print("Error running notification service: " .. updated)
 36 |     self._running = false
 37 |     return
 38 |   end
 39 |   if not updated then
 40 |     self._running = false
 41 |     return
 42 |   end
 43 |   vim.defer_fn(function()
 44 |     self:_run()
 45 |   end, 1000 / self._fps)
 46 | end
 47 | 
 48 | ---@param notif notify.Notification;q
 49 | ---@return integer
 50 | function NotificationService:push(notif)
 51 |   local buf = vim.api.nvim_create_buf(false, true)
 52 |   local notif_buf = NotificationBuf(buf, notif, { config = self._config })
 53 |   notif_buf:render()
 54 |   self._buffers[notif.id] = notif_buf
 55 |   self._pending:push(notif_buf)
 56 |   if not self._running then
 57 |     self:_run()
 58 |   end
 59 |   return buf
 60 | end
 61 | 
 62 | function NotificationService:replace(id, notif)
 63 |   local existing = self._buffers[id]
 64 |   if not (existing and existing:is_valid()) then
 65 |     vim.notify("No matching notification found to replace")
 66 |     return
 67 |   end
 68 |   existing:set_notification(notif)
 69 |   self._buffers[id] = nil
 70 |   self._buffers[notif.id] = existing
 71 |   pcall(existing.render, existing)
 72 |   local win = vim.fn.bufwinid(existing:buffer())
 73 |   if win ~= -1 then
 74 |     -- Highlights can change name if level changed so we have to re-link
 75 |     -- vim.wo does not behave like setlocal, thus we use setwinvar to set a
 76 |     -- local option. Otherwise our changes would affect subsequently opened
 77 |     -- windows.
 78 |     -- see e.g. neovim#14595
 79 |     vim.fn.setwinvar(
 80 |       win,
 81 |       "&winhl",
 82 |       "NormalNC:NONE" .. ",Normal:" .. existing.highlights.body .. ",FloatBorder:" .. existing.highlights.border
 83 |     )
 84 |     vim.api.nvim_win_set_option(win, 'cursorcolumn', false)
 85 |     vim.api.nvim_win_set_option(win, 'cursorline', false)
 86 | 
 87 |     vim.api.nvim_win_set_width(win, existing:width())
 88 |     vim.api.nvim_win_set_height(win, existing:height())
 89 | 
 90 |     self._animator:on_refresh(win)
 91 |   end
 92 | end
 93 | 
 94 | function NotificationService:dismiss(opts)
 95 |   local notif_wins = vim.tbl_keys(self._animator.win_stages)
 96 |   for _, win in pairs(notif_wins) do
 97 |     pcall(vim.api.nvim_win_close, win, true)
 98 |   end
 99 |   if opts.pending then
100 |     local cleared = 0
101 |     while self._pending:pop() do
102 |       cleared = cleared + 1
103 |     end
104 |     if not opts.silent then
105 |       vim.notify("Cleared " .. cleared .. " pending notifications")
106 |     end
107 |   end
108 | end
109 | 
110 | function NotificationService:pending()
111 |   return self._pending:length()
112 | end
113 | 
114 | ---@return NotificationService
115 | return function(config, animator)
116 |   return NotificationService:new(config, animator)
117 | end
118 | 


--------------------------------------------------------------------------------
/lua/notify/service/notification.lua:
--------------------------------------------------------------------------------
 1 | ---@class notify.Notification
 2 | ---@field id integer
 3 | ---@field level string
 4 | ---@field message string[]
 5 | ---@field timeout number | nil
 6 | ---@field title string[]
 7 | ---@field icon string
 8 | ---@field time number
 9 | ---@field width number
10 | ---@field animate boolean
11 | ---@field hide_from_history boolean
12 | ---@field keep fun(): boolean
13 | ---@field on_open fun(win: number, record: notify.Record) | nil
14 | ---@field on_close fun(win: number, record: notify.Record) | nil
15 | ---@field render fun(buf: integer, notification: notify.Notification, highlights: table<string, string>)
16 | ---@field duplicates? integer[] shared list of duplicate notifications by id
17 | local Notification = {}
18 | 
19 | local level_maps = vim.tbl_extend("keep", {}, vim.log.levels)
20 | for k, v in pairs(vim.log.levels) do
21 |   level_maps[v] = k
22 | end
23 | 
24 | function Notification:new(id, message, level, opts, config)
25 |   if type(level) == "number" then
26 |     level = level_maps[level]
27 |   end
28 |   if type(message) == "string" then
29 |     message = vim.split(message, "\n")
30 |   end
31 |   level = vim.fn.toupper(level or "info")
32 |   local time = vim.fn.localtime()
33 |   local title = opts.title or ""
34 |   if type(title) == "string" then
35 |     title = { title, vim.fn.strftime(config.time_formats().notification, time) }
36 |   end
37 |   vim.validate('message', message, "table")
38 |   vim.validate('level', level, "string")
39 |   vim.validate('title', title, "table")
40 |   local notif = {
41 |     id = id,
42 |     message = message,
43 |     title = title,
44 |     icon = opts.icon or config.icons()[level] or config.icons().INFO,
45 |     time = time,
46 |     timeout = opts.timeout,
47 |     level = level,
48 |     keep = opts.keep,
49 |     on_open = opts.on_open,
50 |     on_close = opts.on_close,
51 |     animate = opts.animate ~= false,
52 |     render = opts.render,
53 |     hide_from_history = opts.hide_from_history,
54 |     duplicates = opts.duplicates,
55 |   }
56 |   self.__index = self
57 |   setmetatable(notif, self)
58 |   return notif
59 | end
60 | 
61 | function Notification:record()
62 |   return {
63 |     id = self.id,
64 |     message = self.message,
65 |     level = self.level,
66 |     time = self.time,
67 |     title = self.title,
68 |     icon = self.icon,
69 |     render = self.render,
70 |   }
71 | end
72 | 
73 | ---@param message string | string[]
74 | ---@param level string | number
75 | ---@param opts notify.Options
76 | return function(id, message, level, opts, config)
77 |   return Notification:new(id, message, level, opts, config)
78 | end
79 | 


--------------------------------------------------------------------------------
/lua/notify/stages/fade.lua:
--------------------------------------------------------------------------------
 1 | local stages_util = require("notify.stages.util")
 2 | 
 3 | return function(direction)
 4 |   return {
 5 |     function(state)
 6 |       local next_height = state.message.height + 2
 7 |       local next_row = stages_util.available_slot(state.open_windows, next_height, direction)
 8 |       if not next_row then
 9 |         return nil
10 |       end
11 |       return {
12 |         relative = "editor",
13 |         anchor = "NE",
14 |         width = state.message.width,
15 |         height = state.message.height,
16 |         col = vim.opt.columns:get(),
17 |         row = next_row,
18 |         border = "rounded",
19 |         style = "minimal",
20 |         opacity = 0,
21 |       }
22 |     end,
23 |     function()
24 |       return {
25 |         opacity = { 100 },
26 |         col = { vim.opt.columns:get() },
27 |       }
28 |     end,
29 |     function()
30 |       return {
31 |         col = { vim.opt.columns:get() },
32 |         time = true,
33 |       }
34 |     end,
35 |     function()
36 |       return {
37 |         opacity = {
38 |           0,
39 |           frequency = 2,
40 |           complete = function(cur_opacity)
41 |             return cur_opacity <= 4
42 |           end,
43 |         },
44 |         col = { vim.opt.columns:get() },
45 |       }
46 |     end,
47 |   }
48 | end
49 | 


--------------------------------------------------------------------------------
/lua/notify/stages/fade_in_slide_out.lua:
--------------------------------------------------------------------------------
 1 | local stages_util = require("notify.stages.util")
 2 | 
 3 | return function(direction)
 4 |   return {
 5 |     function(state)
 6 |       local next_height = state.message.height + 2
 7 |       local next_row = stages_util.available_slot(state.open_windows, next_height, direction)
 8 |       if not next_row then
 9 |         return nil
10 |       end
11 |       return {
12 |         relative = "editor",
13 |         anchor = "NE",
14 |         width = state.message.width,
15 |         height = state.message.height,
16 |         col = vim.opt.columns:get(),
17 |         row = next_row,
18 |         border = "rounded",
19 |         style = "minimal",
20 |         opacity = 0,
21 |       }
22 |     end,
23 |     function(state, win)
24 |       return {
25 |         opacity = { 100 },
26 |         col = { vim.opt.columns:get() },
27 |         row = {
28 |           stages_util.slot_after_previous(win, state.open_windows, direction),
29 |           frequency = 3,
30 |           complete = function()
31 |             return true
32 |           end,
33 |         },
34 |       }
35 |     end,
36 |     function(state, win)
37 |       return {
38 |         col = { vim.opt.columns:get() },
39 |         time = true,
40 |         row = {
41 |           stages_util.slot_after_previous(win, state.open_windows, direction),
42 |           frequency = 3,
43 |           complete = function()
44 |             return true
45 |           end,
46 |         },
47 |       }
48 |     end,
49 |     function(state, win)
50 |       return {
51 |         width = {
52 |           1,
53 |           frequency = 2.5,
54 |           damping = 0.9,
55 |           complete = function(cur_width)
56 |             return cur_width < 3
57 |           end,
58 |         },
59 |         opacity = {
60 |           0,
61 |           frequency = 2,
62 |           complete = function(cur_opacity)
63 |             return cur_opacity <= 4
64 |           end,
65 |         },
66 |         col = { vim.opt.columns:get() },
67 |         row = {
68 |           stages_util.slot_after_previous(win, state.open_windows, direction),
69 |           frequency = 3,
70 |           complete = function()
71 |             return true
72 |           end,
73 |         },
74 |       }
75 |     end,
76 |   }
77 | end
78 | 


--------------------------------------------------------------------------------
/lua/notify/stages/init.lua:
--------------------------------------------------------------------------------
 1 | local M = {}
 2 | 
 3 | ---@class MessageState
 4 | ---@field width number
 5 | ---@field height number
 6 | 
 7 | ---@alias InitStage fun(open_windows: number[], message_state: MessageState): table | nil
 8 | ---@alias AnimationStage fun(win: number, message_state: MessageState): table
 9 | 
10 | ---@alias Stage InitStage | AnimationStage
11 | ---@alias Stages Stage[]
12 | 
13 | setmetatable(M, {
14 |   ---@return Stages
15 |   __index = function(_, key)
16 |     return require("notify.stages." .. key)
17 |   end,
18 | })
19 | 
20 | return M
21 | 


--------------------------------------------------------------------------------
/lua/notify/stages/no_animation.lua:
--------------------------------------------------------------------------------
 1 | local stages_util = require("notify.stages.util")
 2 | 
 3 | return function(direction)
 4 |   return {
 5 |     function(state)
 6 |       local next_height = state.message.height + 2
 7 |       local next_row = stages_util.available_slot(state.open_windows, next_height, direction)
 8 |       if not next_row then
 9 |         return nil
10 |       end
11 |       return {
12 |         relative = "editor",
13 |         anchor = "NE",
14 |         width = state.message.width,
15 |         height = state.message.height,
16 |         col = vim.opt.columns:get(),
17 |         row = next_row,
18 |         border = "rounded",
19 |         style = "minimal",
20 |       }
21 |     end,
22 |     function(state, win)
23 |       return {
24 |         col = vim.opt.columns:get(),
25 |         time = true,
26 |         row = stages_util.slot_after_previous(win, state.open_windows, direction),
27 |       }
28 |     end,
29 |   }
30 | end
31 | 


--------------------------------------------------------------------------------
/lua/notify/stages/slide.lua:
--------------------------------------------------------------------------------
 1 | local stages_util = require("notify.stages.util")
 2 | 
 3 | return function(direction)
 4 |   return {
 5 |     function(state)
 6 |       local next_height = state.message.height + 2
 7 |       local next_row = stages_util.available_slot(state.open_windows, next_height, direction)
 8 |       if not next_row then
 9 |         return nil
10 |       end
11 |       return {
12 |         relative = "editor",
13 |         anchor = "NE",
14 |         width = 1,
15 |         height = state.message.height,
16 |         col = vim.opt.columns:get(),
17 |         row = next_row,
18 |         border = "rounded",
19 |         style = "minimal",
20 |       }
21 |     end,
22 |     function(state)
23 |       return {
24 |         width = { state.message.width, frequency = 2 },
25 |         col = { vim.opt.columns:get() },
26 |       }
27 |     end,
28 |     function()
29 |       return {
30 |         col = { vim.opt.columns:get() },
31 |         time = true,
32 |       }
33 |     end,
34 |     function()
35 |       return {
36 |         width = {
37 |           1,
38 |           frequency = 2.5,
39 |           damping = 0.9,
40 |           complete = function(cur_width)
41 |             return cur_width < 2
42 |           end,
43 |         },
44 |         col = { vim.opt.columns:get() },
45 |       }
46 |     end,
47 |   }
48 | end
49 | 


--------------------------------------------------------------------------------
/lua/notify/stages/slide_out.lua:
--------------------------------------------------------------------------------
 1 | local stages_util = require("notify.stages.util")
 2 | 
 3 | return function(direction)
 4 |   return {
 5 |     function(state)
 6 |       local next_height = state.message.height + 2
 7 |       local next_row = stages_util.available_slot(state.open_windows, next_height, direction)
 8 |       if not next_row then
 9 |         return nil
10 |       end
11 |       return {
12 |         relative = "editor",
13 |         anchor = "NE",
14 |         width = state.message.width,
15 |         height = state.message.height,
16 |         col = vim.opt.columns:get(),
17 |         row = next_row,
18 |         border = "rounded",
19 |         style = "minimal",
20 |       }
21 |     end,
22 |     function(state, win)
23 |       return {
24 |         col = vim.opt.columns:get(),
25 |         time = true,
26 |         row = stages_util.slot_after_previous(win, state.open_windows, direction),
27 |       }
28 |     end,
29 |     function(state, win)
30 |       return {
31 |         width = {
32 |           1,
33 |           frequency = 2.5,
34 |           damping = 0.9,
35 |           complete = function(cur_width)
36 |             return cur_width < 3
37 |           end,
38 |         },
39 |         col = { vim.opt.columns:get() },
40 |         row = {
41 |           stages_util.slot_after_previous(win, state.open_windows, direction),
42 |           frequency = 3,
43 |           complete = function()
44 |             return true
45 |           end,
46 |         },
47 |       }
48 |     end,
49 |   }
50 | end
51 | 


--------------------------------------------------------------------------------
/lua/notify/stages/static.lua:
--------------------------------------------------------------------------------
 1 | local stages_util = require("notify.stages.util")
 2 | 
 3 | return function(direction)
 4 |   return {
 5 |     function(state)
 6 |       local next_height = state.message.height + 2
 7 |       local next_row = stages_util.available_slot(state.open_windows, next_height, direction)
 8 |       if not next_row then
 9 |         return nil
10 |       end
11 |       return {
12 |         relative = "editor",
13 |         anchor = "NE",
14 |         width = state.message.width,
15 |         height = state.message.height,
16 |         col = vim.opt.columns:get(),
17 |         row = next_row,
18 |         border = "rounded",
19 |         style = "minimal",
20 |       }
21 |     end,
22 |     function()
23 |       return {
24 |         col = vim.opt.columns:get(),
25 |         time = true,
26 |       }
27 |     end,
28 |   }
29 | end
30 | 


--------------------------------------------------------------------------------
/lua/notify/stages/util.lua:
--------------------------------------------------------------------------------
  1 | local max, min = math.max, math.min
  2 | local util = require("notify.util")
  3 | 
  4 | local M = {}
  5 | 
  6 | M.DIRECTION = {
  7 |   TOP_DOWN = "top_down",
  8 |   BOTTOM_UP = "bottom_up",
  9 |   LEFT_RIGHT = "left_right",
 10 |   RIGHT_LEFT = "right_left",
 11 | }
 12 | 
 13 | local function is_increasing(direction)
 14 |   return (direction == M.DIRECTION.TOP_DOWN or direction == M.DIRECTION.LEFT_RIGHT)
 15 | end
 16 | 
 17 | local function moves_vertically(direction)
 18 |   return (direction == M.DIRECTION.TOP_DOWN or direction == M.DIRECTION.BOTTOM_UP)
 19 | end
 20 | 
 21 | function M.slot_name(direction)
 22 |   if moves_vertically(direction) then
 23 |     return "height"
 24 |   end
 25 |   return "width"
 26 | end
 27 | 
 28 | local function less(a, b)
 29 |   return a < b
 30 | end
 31 | 
 32 | local function greater(a, b)
 33 |   return a > b
 34 | end
 35 | 
 36 | local function overlaps(a, b)
 37 |   return a.min <= b.max and b.min <= a.max
 38 | end
 39 | 
 40 | local move_slot = function(direction, slot, delta)
 41 |   if is_increasing(direction) then
 42 |     return slot + delta
 43 |   end
 44 |   return slot - delta
 45 | end
 46 | 
 47 | local function slot_key(direction)
 48 |   return moves_vertically(direction) and "row" or "col"
 49 | end
 50 | 
 51 | local function space_key(direction)
 52 |   return moves_vertically(direction) and "height" or "width"
 53 | end
 54 | 
 55 | -- TODO: Use direction to check border lists
 56 | local function border_padding(direction, win_conf)
 57 |   if not win_conf.border or win_conf.border == "none" then
 58 |     return 0
 59 |   end
 60 |   return 2
 61 | end
 62 | 
 63 | ---@param windows number[]
 64 | ---@param direction string
 65 | ---@return { max: integer, min: integer}[]
 66 | local function window_intervals(windows, direction, cmp)
 67 |   local win_intervals = {}
 68 |   for _, w in ipairs(windows) do
 69 |     local exists, existing_conf = util.get_win_config(w)
 70 |     if exists then
 71 |       local border_space = border_padding(direction, existing_conf)
 72 |       win_intervals[#win_intervals + 1] = {
 73 |         min = existing_conf[slot_key(direction)],
 74 |         max = existing_conf[slot_key(direction)]
 75 |           + existing_conf[space_key(direction)]
 76 |           + border_space
 77 |           - 1,
 78 |       }
 79 |     end
 80 |   end
 81 |   table.sort(win_intervals, function(a, b)
 82 |     return cmp(a.min, b.min)
 83 |   end)
 84 |   return win_intervals
 85 | end
 86 | 
 87 | function M.get_slot_range(direction)
 88 |   local top = 0
 89 |   if vim.o.showtabline == 2 or (vim.o.showtabline == 1 and vim.fn.tabpagenr("
quot;) > 1) then
 90 |     top = 1
 91 |   end
 92 |   if vim.wo.winbar ~= '' then
 93 |     top = top + 1
 94 |   end
 95 | 
 96 |   local bottom = vim.opt.lines:get()
 97 |     - (vim.opt.cmdheight:get() + (vim.opt.laststatus:get() > 0 and 1 or 0) + 1)
 98 |   local left = 1
 99 |   local right = vim.opt.columns:get()
100 |   if M.DIRECTION.TOP_DOWN == direction then
101 |     return top, bottom
102 |   elseif M.DIRECTION.BOTTOM_UP == direction then
103 |     return bottom, top
104 |   elseif M.DIRECTION.LEFT_RIGHT == direction then
105 |     return left, right
106 |   elseif M.DIRECTION.RIGHT_LEFT == direction then
107 |     return right, left
108 |   end
109 |   error(string.format("Invalid direction: %s", direction))
110 | end
111 | 
112 | ---@param existing_wins number[] Windows to avoid overlapping
113 | ---@param required_space number Window height or width including borders
114 | ---@param direction string Direction to stack windows, one of M.DIRECTION
115 | ---@return number | nil Slot to place window at or nil if no slot available
116 | function M.available_slot(existing_wins, required_space, direction)
117 |   local increasing = is_increasing(direction)
118 |   local cmp = increasing and less or greater
119 |   local first_slot, last_slot = M.get_slot_range(direction)
120 | 
121 |   local function create_interval(start_slot)
122 |     local end_slot = move_slot(direction, start_slot, required_space - 1)
123 |     return { min = min(start_slot, end_slot), max = max(start_slot, end_slot) }
124 |   end
125 | 
126 |   local interval = create_interval(first_slot)
127 | 
128 |   local intervals = window_intervals(existing_wins, direction, cmp)
129 | 
130 |   for _, next_interval in ipairs(intervals) do
131 |     if overlaps(next_interval, interval) then
132 |       interval = create_interval(
133 |         move_slot(direction, increasing and next_interval.max or next_interval.min, 1)
134 |       )
135 |     end
136 |   end
137 | 
138 |   if #intervals > 0 and not cmp(is_increasing and interval.max or interval.min, last_slot) then
139 |     return nil
140 |   end
141 | 
142 |   return interval.min
143 | end
144 | 
145 | ---Gets the next slot available for the given window while maintaining its position using the given list.
146 | ---@param win number
147 | ---@param open_windows number[]
148 | ---@param direction string
149 | function M.slot_after_previous(win, open_windows, direction)
150 |   local key = slot_key(direction)
151 |   local cmp = is_increasing(direction) and less or greater
152 |   local exists, cur_win_conf = util.get_win_config(win)
153 |   if not exists then
154 |     return 0
155 |   end
156 | 
157 |   local cur_slot = cur_win_conf[key]
158 |   local win_confs = {}
159 |   for _, w in ipairs(open_windows) do
160 |     local success, conf = util.get_win_config(w)
161 |     if success then
162 |       win_confs[w] = conf
163 |     end
164 |   end
165 | 
166 |   local preceding_wins = vim.tbl_filter(function(open_win)
167 |     return win_confs[open_win] and cmp(win_confs[open_win][key], cur_slot)
168 |   end, open_windows)
169 | 
170 |   if #preceding_wins == 0 then
171 |     local start = M.get_slot_range(direction)
172 |     if is_increasing(direction) then
173 |       return start
174 |     end
175 |     return move_slot(
176 |       direction,
177 |       start,
178 |       cur_win_conf[space_key(direction)] + border_padding(direction, cur_win_conf) / 2
179 |     )
180 |   end
181 | 
182 |   table.sort(preceding_wins, function(a, b)
183 |     return cmp(win_confs[a][key], win_confs[b][key])
184 |   end)
185 | 
186 |   local last_win = preceding_wins[#preceding_wins]
187 |   local last_win_conf = win_confs[last_win]
188 | 
189 |   if is_increasing(direction) then
190 |     return move_slot(
191 |       direction,
192 |       last_win_conf[key],
193 |       last_win_conf[space_key(direction)] + border_padding(direction, last_win_conf)
194 |     )
195 |   else
196 |     return move_slot(
197 |       direction,
198 |       last_win_conf[key],
199 |       cur_win_conf[space_key(direction)] + border_padding(direction, cur_win_conf)
200 |     )
201 |   end
202 | end
203 | 
204 | return M
205 | 


--------------------------------------------------------------------------------
/lua/notify/util/init.lua:
--------------------------------------------------------------------------------
  1 | local M = {}
  2 | 
  3 | local min, max, floor = math.min, math.max, math.floor
  4 | local rshift, lshift, band, bor = bit.rshift, bit.lshift, bit.band, bit.bor
  5 | local strwidth = vim.api.nvim_strwidth or vim.fn.strchars
  6 | 
  7 | function M.is_callable(obj)
  8 |   return type(obj) == "function" or (type(obj) == "table" and obj.__call)
  9 | end
 10 | 
 11 | function M.lazy_require(require_path)
 12 |   return setmetatable({}, {
 13 |     __call = function(_, ...)
 14 |       return require(require_path)(...)
 15 |     end,
 16 |     __index = function(_, key)
 17 |       return require(require_path)[key]
 18 |     end,
 19 |     __newindex = function(_, key, value)
 20 |       require(require_path)[key] = value
 21 |     end,
 22 |   })
 23 | end
 24 | 
 25 | function M.pop(tbl, key, default)
 26 |   local val = default
 27 |   if tbl[key] then
 28 |     val = tbl[key]
 29 |     tbl[key] = nil
 30 |   end
 31 |   return val
 32 | end
 33 | 
 34 | function M.blend(fg_hex, bg_hex, alpha)
 35 |   local segment = 0xFF0000
 36 |   local result = 0
 37 |   for i = 2, 0, -1 do
 38 |     local blended = alpha * rshift(band(fg_hex, segment), i * 8)
 39 |       + (1 - alpha) * rshift(band(bg_hex, segment), i * 8)
 40 | 
 41 |     result = bor(lshift(result, 8), floor((min(max(blended, 0), 255)) + 0.5))
 42 |     segment = rshift(segment, 8)
 43 |   end
 44 | 
 45 |   return result
 46 | end
 47 | 
 48 | function M.round(num, decimals)
 49 |   if decimals then
 50 |     return tonumber(string.format("%." .. decimals .. "f", num))
 51 |   end
 52 |   return math.floor(num + 0.5)
 53 | end
 54 | 
 55 | function M.partial(func, ...)
 56 |   local args = { ... }
 57 |   return function(...)
 58 |     local final = {}
 59 |     vim.list_extend(final, args)
 60 |     vim.list_extend(final, { ... })
 61 |     return func(unpack(final))
 62 |   end
 63 | end
 64 | 
 65 | function M.get_win_config(win)
 66 |   local success, conf = pcall(vim.api.nvim_win_get_config, win)
 67 |   if not success or not conf.row then
 68 |     return false, conf
 69 |   end
 70 |   if type(conf.row) == "table" then
 71 |     conf.row = conf.row[false]
 72 |   end
 73 |   if type(conf.col) == "table" then
 74 |     conf.col = conf.col[false]
 75 |   end
 76 |   return success, conf
 77 | end
 78 | 
 79 | function M.open_win(notif_buf, enter, opts)
 80 |   local win = vim.api.nvim_open_win(notif_buf:buffer(), enter, opts)
 81 |   -- vim.wo does not behave like setlocal, thus we use setwinvar to set local
 82 |   -- only options. Otherwise our changes would affect subsequently opened
 83 |   -- windows.
 84 |   -- see e.g. neovim#14595
 85 |   vim.fn.setwinvar(
 86 |     win,
 87 |     "&winhl",
 88 |     "NormalNC:NONE" .. ",Normal:" .. notif_buf.highlights.body .. ",FloatBorder:" .. notif_buf.highlights.border
 89 |   )
 90 |   vim.fn.setwinvar(win, "&wrap", 0)
 91 |   return win
 92 | end
 93 | 
 94 | M.FIFOQueue = require("notify.util.queue")
 95 | 
 96 | function M.rgb_to_numbers(s)
 97 |   local colours = {}
 98 |   for a in string.gmatch(s, "[A-Fa-f0-9][A-Fa-f0-9]") do
 99 |     colours[#colours + 1] = tonumber(a, 16)
100 |   end
101 |   return colours
102 | end
103 | 
104 | function M.numbers_to_rgb(colours)
105 |   local colour = "#"
106 |   for _, num in pairs(colours) do
107 |     colour = colour .. string.format("%X", num)
108 |   end
109 |   return colour
110 | end
111 | 
112 | function M.highlight(name, fields)
113 |   local fields_string = ""
114 |   for field, value in pairs(fields) do
115 |     fields_string = fields_string .. " " .. field .. "=" .. value
116 |   end
117 |   if fields_string ~= "" then
118 |     vim.cmd("hi " .. name .. fields_string)
119 |   end
120 | end
121 | 
122 | --- Calculate the max render width of a message
123 | ---@param msg string[]|nil
124 | ---@return integer
125 | function M.max_line_width(msg)
126 |   local width = 0
127 | 
128 |   if msg then
129 |     for i = 1, #msg do
130 |       width = max(width, strwidth(msg[i]))
131 |     end
132 |   end
133 | 
134 |   return width
135 | end
136 | 
137 | return M
138 | 


--------------------------------------------------------------------------------
/lua/notify/util/queue.lua:
--------------------------------------------------------------------------------
 1 | ---@class FIFOQueue
 2 | local FIFOQueue = {}
 3 | 
 4 | function FIFOQueue:pop()
 5 |   if self:is_empty() then
 6 |     return nil
 7 |   end
 8 |   local r = self[self.pop_from]
 9 |   self[self.pop_from] = nil
10 |   self.pop_from = self.pop_from - 1
11 |   return r
12 | end
13 | 
14 | function FIFOQueue:peek()
15 |   return self[self.pop_from]
16 | end
17 | 
18 | function FIFOQueue:push(val)
19 |   self[self.push_to] = val
20 |   self.push_to = self.push_to - 1
21 | end
22 | 
23 | function FIFOQueue:is_empty()
24 |   return self:length() == 0
25 | end
26 | 
27 | function FIFOQueue:length()
28 |   return self.pop_from - self.push_to
29 | end
30 | 
31 | function FIFOQueue:iter()
32 |   local i = self.pop_from + 1
33 |   return function()
34 |     if i > self.push_to + 1 then
35 |       i = i - 1
36 |       return self[i]
37 |     end
38 |   end
39 | end
40 | 
41 | function FIFOQueue:new()
42 |   local queue = {
43 |     pop_from = 1,
44 |     push_to = 1,
45 |   }
46 |   self.__index = self
47 |   setmetatable(queue, self)
48 |   return queue
49 | end
50 | 
51 | ---@return FIFOQueue
52 | return function()
53 |   return FIFOQueue:new()
54 | end
55 | 


--------------------------------------------------------------------------------
/lua/notify/windows/init.lua:
--------------------------------------------------------------------------------
  1 | local api = vim.api
  2 | local animate = require("notify.animate")
  3 | local util = require("notify.util")
  4 | local round = util.round
  5 | local max = math.max
  6 | 
  7 | ---@class WindowAnimator
  8 | ---@field config table
  9 | ---@field win_states table<number, table<string, SpringState>>
 10 | ---@field win_stages table<number, integer>
 11 | ---@field notif_bufs table<number, NotificationBuf>
 12 | ---@field timers table
 13 | ---@field stages table
 14 | local WindowAnimator = {}
 15 | 
 16 | function WindowAnimator:new(stages, config)
 17 |   local animator = {
 18 |     config = config,
 19 |     win_stages = {},
 20 |     win_states = {},
 21 |     notif_bufs = {},
 22 |     timers = {},
 23 |     stages = stages,
 24 |   }
 25 |   self.__index = self
 26 |   setmetatable(animator, self)
 27 |   return animator
 28 | end
 29 | 
 30 | function WindowAnimator:render(queue, time)
 31 |   self:push_pending(queue)
 32 |   if vim.tbl_isempty(self.win_stages) then
 33 |     return false
 34 |   end
 35 |   local open_windows = vim.tbl_keys(self.win_stages)
 36 |   for win, _ in pairs(self.win_stages) do
 37 |     self:_update_window(time, win, open_windows)
 38 |   end
 39 |   return true
 40 | end
 41 | 
 42 | function WindowAnimator:push_pending(queue)
 43 |   if queue:is_empty() then
 44 |     return
 45 |   end
 46 |   while not queue:is_empty() do
 47 |     ---@type NotificationBuf
 48 |     local notif_buf = queue:peek()
 49 |     if not notif_buf:is_valid() then
 50 |       queue:pop()
 51 |     else
 52 |       local windows = vim.tbl_keys(self.win_stages)
 53 |       local win_opts = self.stages[1]({
 54 |         message = self:_get_dimensions(notif_buf),
 55 |         open_windows = windows,
 56 |       })
 57 |       if not win_opts then
 58 |         return
 59 |       end
 60 |       local opacity = util.pop(win_opts, "opacity")
 61 |       if opacity then
 62 |         notif_buf.highlights:set_opacity(opacity)
 63 |       end
 64 |       win_opts.noautocmd = true
 65 |       local win = util.open_win(notif_buf, false, win_opts)
 66 |       vim.fn.setwinvar(
 67 |         win,
 68 |         "&winhl",
 69 |         "NormalNC:NONE" .. ",Normal:" .. notif_buf.highlights.body .. ",FloatBorder:" .. notif_buf.highlights.border
 70 |       )
 71 |       self.win_stages[win] = 2
 72 |       self.win_states[win] = {}
 73 |       self.notif_bufs[win] = notif_buf
 74 |       queue:pop()
 75 |       notif_buf:open(win)
 76 |     end
 77 |   end
 78 | end
 79 | 
 80 | function WindowAnimator:_advance_win_stage(win)
 81 |   local cur_stage = self.win_stages[win]
 82 |   if not cur_stage then
 83 |     return
 84 |   end
 85 |   if cur_stage < #self.stages then
 86 |     if api.nvim_get_current_win() == win then
 87 |       return
 88 |     end
 89 |     self.win_stages[win] = cur_stage + 1
 90 |     return
 91 |   end
 92 | 
 93 |   self.win_stages[win] = nil
 94 | 
 95 |   local function close()
 96 |     if api.nvim_get_current_win() == win then
 97 |       return vim.defer_fn(close, 1000)
 98 |     end
 99 |     self:_remove_win(win)
100 |   end
101 | 
102 |   close()
103 | end
104 | 
105 | function WindowAnimator:_remove_win(win)
106 |   pcall(api.nvim_win_close, win, true)
107 |   self.win_stages[win] = nil
108 |   self.win_states[win] = nil
109 |   local notif_buf = self.notif_bufs[win]
110 |   self.notif_bufs[win] = nil
111 |   notif_buf:close(win)
112 | end
113 | 
114 | function WindowAnimator:on_refresh(win)
115 |   local notif_buf = self.notif_bufs[win]
116 |   if not notif_buf then
117 |     return
118 |   end
119 |   if self.timers[win] then
120 |     self.timers[win]:set_repeat(notif_buf:timeout() or self.config.default_timeout())
121 |     self.timers[win]:again()
122 |   end
123 | end
124 | 
125 | function WindowAnimator:_start_timer(win)
126 |   local buf_time = self.notif_bufs[win]:timeout() == nil and self.config.default_timeout()
127 |     or self.notif_bufs[win]:timeout()
128 |   if buf_time ~= false then
129 |     if buf_time == true then
130 |       buf_time = nil
131 |     end
132 |     local timer = vim.loop.new_timer()
133 |     self.timers[win] = timer
134 |     timer:start(
135 |       buf_time,
136 |       buf_time,
137 |       vim.schedule_wrap(function()
138 |         timer:stop()
139 |         self.timers[win] = nil
140 |         local notif_buf = self.notif_bufs[win]
141 |         if notif_buf and notif_buf:should_stay() then
142 |           return
143 |         end
144 |         self:_advance_win_stage(win)
145 |       end)
146 |     )
147 |   end
148 | end
149 | 
150 | function WindowAnimator:_update_window(time, win, open_windows)
151 |   local stage = self.win_stages[win]
152 |   local notif_buf = self.notif_bufs[win]
153 |   local win_goals = self:_get_win_goals(win, stage, open_windows)
154 | 
155 |   if not win_goals then
156 |     self:_remove_win(win)
157 |   end
158 | 
159 |   -- If we don't animate, then we move to all goals instantly.
160 |   -- Can't just jump to the end, because we need to the intermediate changes
161 |   while
162 |     not notif_buf:should_animate()
163 |     and win_goals.time == nil
164 |     and self.win_stages[win] < #self.stages
165 |   do
166 |     for field, goal in pairs(win_goals) do
167 |       if type(goal) == "table" then
168 |         win_goals[field] = goal[1]
169 |       end
170 |     end
171 |     self:_advance_win_state(win, win_goals, time)
172 |     self:_advance_win_stage(win)
173 |     stage = self.win_stages[win]
174 |     win_goals = self:_get_win_goals(win, stage, open_windows)
175 |   end
176 | 
177 |   if win_goals.time and not self.timers[win] then
178 |     self:_start_timer(win)
179 |   end
180 | 
181 |   self:_advance_win_state(win, win_goals, time)
182 | 
183 |   if self:_is_complete(win, win_goals) and not win_goals.time then
184 |     self:_advance_win_stage(win)
185 |   end
186 | end
187 | 
188 | function WindowAnimator:_is_complete(win, goals)
189 |   local complete = true
190 |   local win_state = self.win_states[win]
191 |   if not win_state then
192 |     return true
193 |   end
194 |   for field, goal in pairs(goals) do
195 |     if field ~= "time" then
196 |       if type(goal) == "table" then
197 |         if goal.complete then
198 |           complete = goal.complete(win_state[field].position)
199 |         else
200 |           complete = goal[1] == round(win_state[field].position, 2)
201 |         end
202 |       end
203 |       if not complete then
204 |         break
205 |       end
206 |     end
207 |   end
208 |   return complete
209 | end
210 | 
211 | function WindowAnimator:_advance_win_state(win, goals, time)
212 |   local win_state = self.win_states[win]
213 | 
214 |   local win_configs = {}
215 | 
216 |   local function win_conf(win_)
217 |     if win_configs[win_] then
218 |       return win_configs[win_]
219 |     end
220 |     local exists, conf = util.get_win_config(win_)
221 |     if not exists then
222 |       self:_remove_win(win_)
223 |       return
224 |     end
225 |     win_configs[win_] = conf
226 |     return conf
227 |   end
228 | 
229 |   for field, goal in pairs(goals) do
230 |     if field ~= "time" then
231 |       local goal_type = type(goal)
232 |       -- Handle spring goal
233 |       if goal_type == "table" and goal[1] then
234 |         if not win_state[field] then
235 |           if field == "opacity" then
236 |             win_state[field] = { position = self.notif_bufs[win].highlights:get_opacity() }
237 |           else
238 |             local conf = win_conf(win)
239 |             if not conf then
240 |               return true
241 |             end
242 |             win_state[field] = { position = conf[field] }
243 |           end
244 |         end
245 |         animate.spring(time, goal[1], win_state[field], goal.frequency or 1, goal.damping or 1)
246 |         --- Directly move goal
247 |       elseif goal_type ~= "table" then
248 |         win_state[field] = { position = goal }
249 |       else
250 |         error("nvim-notify: Invalid stage goal: " .. vim.inspect(goal))
251 |       end
252 |     end
253 |   end
254 | 
255 |   return self:_apply_win_state(win, win_state)
256 | end
257 | 
258 | function WindowAnimator:_get_win_goals(win, win_stage, open_windows)
259 |   local notif_buf = self.notif_bufs[win]
260 |   local win_goals = self.stages[win_stage]({
261 |     buffer = notif_buf:buffer(),
262 |     message = self:_get_dimensions(notif_buf),
263 |     open_windows = open_windows,
264 |   }, win)
265 |   return win_goals
266 | end
267 | 
268 | function WindowAnimator:_get_dimensions(notif_buf)
269 |   return {
270 |     height = math.min(self.config.max_height() or 1000, notif_buf:height()),
271 |     width = math.min(self.config.max_width() or 1000, notif_buf:width()),
272 |   }
273 | end
274 | 
275 | function WindowAnimator:_apply_win_state(win, win_state)
276 |   local hl_updated = false
277 |   if win_state.opacity then
278 |     local notif_buf = self.notif_bufs[win]
279 |     if notif_buf:is_valid() then
280 |       hl_updated = notif_buf.highlights:set_opacity(win_state.opacity.position)
281 |       vim.fn.setwinvar(
282 |         win,
283 |         "&winhl",
284 |         "NormalNC:NONE" .. ",Normal:" .. notif_buf.highlights.body .. ",FloatBorder:" .. notif_buf.highlights.border
285 |       )
286 |     end
287 |   end
288 |   local exists, conf = util.get_win_config(win)
289 |   local new_conf = {}
290 |   local win_updated = false
291 |   if not exists then
292 |     self:_remove_win(win)
293 |   else
294 |     local function set_field(field, min, round_to)
295 |       if not win_state[field] then
296 |         return
297 |       end
298 |       local new_value = max(round(win_state[field].position, round_to), min)
299 |       if new_value == conf[field] then
300 |         return
301 |       end
302 |       win_updated = true
303 |       new_conf[field] = new_value
304 |     end
305 | 
306 |     set_field("row", 0, 1)
307 |     set_field("col", 0, 1)
308 |     set_field("width", 1)
309 |     set_field("height", 1)
310 | 
311 |     if win_updated then
312 |       if new_conf.row or new_conf.col then
313 |         new_conf.relative = conf.relative
314 |         new_conf.row = new_conf.row or conf.row
315 |         new_conf.col = new_conf.col or conf.col
316 |       end
317 |       api.nvim_win_set_config(win, new_conf)
318 |     end
319 |   end
320 |   -- The 'flush' key is set to enforce redrawing during blocking event.
321 |   pcall(vim.api.nvim__redraw, { win = win, valid = false, flush = true })
322 |   return hl_updated or win_updated
323 | end
324 | 
325 | ---@return WindowAnimator
326 | return function(stages, config)
327 |   return WindowAnimator:new(stages, config)
328 | end
329 | 


--------------------------------------------------------------------------------
/lua/telescope/_extensions/notify.lua:
--------------------------------------------------------------------------------
  1 | local pickers = require("telescope.pickers")
  2 | local finders = require("telescope.finders")
  3 | local conf = require("telescope.config").values
  4 | local actions = require("telescope.actions")
  5 | local action_state = require("telescope.actions.state")
  6 | local previewers = require("telescope.previewers")
  7 | local entry_display = require("telescope.pickers.entry_display")
  8 | local notify = require("notify")
  9 | 
 10 | local widths = {
 11 |   time = 8,
 12 |   title = nil,
 13 |   icon = nil,
 14 |   level = nil,
 15 |   message = nil,
 16 | }
 17 | 
 18 | local displayer = entry_display.create({
 19 |   separator = " ",
 20 |   items = {
 21 |     { width = widths.time },
 22 |     { width = widths.title },
 23 |     { width = widths.icon },
 24 |     { width = widths.level },
 25 |     { width = widths.message },
 26 |   },
 27 | })
 28 | 
 29 | local telescope_notifications = function(opts)
 30 |   local time_format = require("notify")._config().time_formats().notification
 31 |   local notifs = require("notify").history()
 32 |   local reversed = {}
 33 |   for i, notif in ipairs(notifs) do
 34 |     reversed[#notifs - i + 1] = notif
 35 |   end
 36 |   pickers
 37 |     .new(opts, {
 38 |       results_title = "Notifications",
 39 |       prompt_title = "Filter Notifications",
 40 |       finder = finders.new_table({
 41 |         results = reversed,
 42 |         entry_maker = function(notif)
 43 |           return {
 44 |             value = notif,
 45 |             display = function(entry)
 46 |               return displayer({
 47 |                 { vim.fn.strftime(time_format, entry.value.time), "NotifyLogTime" },
 48 |                 { entry.value.title[1], "NotifyLogTitle" },
 49 |                 { entry.value.icon, "Notify" .. entry.value.level .. "Title" },
 50 |                 { entry.value.level, "Notify" .. entry.value.level .. "Title" },
 51 |                 { entry.value.message[1], "Notify" .. entry.value.level .. "Body" },
 52 |               })
 53 |             end,
 54 |             ordinal = notif.title[1] .. " " .. notif.title[2] .. " " .. table.concat(
 55 |               notif.message,
 56 |               " "
 57 |             ),
 58 |           }
 59 |         end,
 60 |       }),
 61 |       sorter = conf.generic_sorter(opts),
 62 |       attach_mappings = function(prompt_bufnr, map)
 63 |         actions.select_default:replace(function()
 64 |           actions.close(prompt_bufnr)
 65 |           local selection = action_state.get_selected_entry()
 66 |           if selection == nil then
 67 |             return
 68 |           end
 69 | 
 70 |           local notification = selection.value
 71 |           local opened_buffer = notify.open(notification)
 72 | 
 73 |           local lines = vim.opt.lines:get()
 74 |           local cols = vim.opt.columns:get()
 75 | 
 76 |           local win = vim.api.nvim_open_win(opened_buffer.buffer, true, {
 77 |             relative = "editor",
 78 |             row = (lines - opened_buffer.height) / 2,
 79 |             col = (cols - opened_buffer.width) / 2,
 80 |             height = opened_buffer.height,
 81 |             width = opened_buffer.width,
 82 |             border = "rounded",
 83 |             style = "minimal",
 84 |           })
 85 |           -- vim.wo does not behave like setlocal, thus we use setwinvar to set local
 86 |           -- only options. Otherwise our changes would affect subsequently opened
 87 |           -- windows.
 88 |           -- see e.g. neovim#14595
 89 |           vim.fn.setwinvar(
 90 |             win,
 91 |             "&winhl",
 92 |             "Normal:"
 93 |               .. opened_buffer.highlights.body
 94 |               .. ",FloatBorder:"
 95 |               .. opened_buffer.highlights.border
 96 |           )
 97 |           vim.fn.setwinvar(win, "&wrap", 0)
 98 |         end)
 99 |         return true
100 |       end,
101 |       previewer = previewers.new_buffer_previewer({
102 |         title = "Message",
103 |         define_preview = function(self, entry, status)
104 |           local notification = entry.value
105 |           local max_width = vim.api.nvim_win_get_config(status.preview_win).width
106 |           vim.api.nvim_win_set_option(status.preview_win, "wrap", true)
107 |           notify.open(notification, { buffer = self.state.bufnr, max_width = max_width })
108 |         end,
109 |       }),
110 |     })
111 |     :find()
112 | end
113 | 
114 | return require("telescope").register_extension({
115 |   exports = {
116 |     notify = telescope_notifications,
117 |   },
118 | })
119 | 


--------------------------------------------------------------------------------
/scripts/docgen:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | 
3 | nvim --headless -c "luafile ./scripts/gendocs.lua" -c 'qa'
4 | 


--------------------------------------------------------------------------------
/scripts/gendocs.lua:
--------------------------------------------------------------------------------
  1 | -- TODO: A lot of this is private code from minidoc, which could be removed if made public
  2 | 
  3 | local minidoc = require("mini.doc")
  4 | 
  5 | local H = {}
  6 | --stylua: ignore start
  7 | H.pattern_sets = {
  8 |   -- Patterns for working with afterlines. At the moment deliberately crafted
  9 |   -- to work only on first line without indent.
 10 | 
 11 |   -- Determine if line is a function definition. Captures function name and
 12 |   -- arguments. For reference see '2.5.9 – Function Definitions' in Lua manual.
 13 |   afterline_fundef = {
 14 |     '^function%s+(%S-)(%b())', -- Regular definition
 15 |     '^local%s+function%s+(%S-)(%b())', -- Local definition
 16 |     '^(%S+)%s*=%s*function(%b())', -- Regular assignment
 17 |     '^local%s+(%S+)%s*=%s*function(%b())', -- Local assignment
 18 |   },
 19 | 
 20 |   -- Determine if line is a general assignment
 21 |   afterline_assign = {
 22 |     '^(%S-)%s*=', -- General assignment
 23 |     '^local%s+(%S-)%s*=', -- Local assignment
 24 |   },
 25 | 
 26 |   -- Patterns to work with type descriptions
 27 |   -- (see https://github.com/sumneko/lua-language-server/wiki/EmmyLua-Annotations#types-and-type)
 28 |   types = {
 29 |     'table%b<>',
 30 |     'fun%b(): %S+', 'fun%b()', 'async fun%b(): %S+', 'async fun%b()',
 31 |     'nil', 'any', 'boolean', 'string', 'number', 'integer', 'function', 'table', 'thread', 'userdata', 'lightuserdata',
 32 |     '%.%.%.',
 33 |     "%S+",
 34 | 
 35 |   },
 36 | }
 37 | 
 38 | 
 39 | H.apply_config = function(config)
 40 |   MiniDoc.config = config
 41 | end
 42 | 
 43 | H.is_disabled = function()
 44 |   return vim.g.minidoc_disable == true or vim.b.minidoc_disable == true
 45 | end
 46 | 
 47 | H.get_config = function(config)
 48 |   return vim.tbl_deep_extend("force", MiniDoc.config, vim.b.minidoc_config or {}, config or {})
 49 | end
 50 | 
 51 | -- Work with project specific script ==========================================
 52 | H.execute_project_script = function(input, output, config)
 53 |   -- Don't process script if there are more than one active `generate` calls
 54 |   if H.generate_is_active then
 55 |     return
 56 |   end
 57 | 
 58 |   -- Don't process script if at least one argument is not default
 59 |   if not (input == nil and output == nil and config == nil) then
 60 |     return
 61 |   end
 62 | 
 63 |   -- Store information
 64 |   local global_config_cache = vim.deepcopy(MiniDoc.config)
 65 |   local local_config_cache = vim.b.minidoc_config
 66 | 
 67 |   -- Pass information to a possible `generate()` call inside script
 68 |   H.generate_is_active = true
 69 |   H.generate_recent_output = nil
 70 | 
 71 |   -- Execute script
 72 |   local success = pcall(vim.cmd, "luafile " .. H.get_config(config).script_path)
 73 | 
 74 |   -- Restore information
 75 |   MiniDoc.config = global_config_cache
 76 |   vim.b.minidoc_config = local_config_cache
 77 |   H.generate_is_active = nil
 78 | 
 79 |   return success
 80 | end
 81 | 
 82 | -- Default documentation targets ----------------------------------------------
 83 | H.default_input = function()
 84 |   -- Search in current and recursively in other directories for files with
 85 |   -- 'lua' extension
 86 |   local res = {}
 87 |   for _, dir_glob in ipairs({ ".", "lua/**", "after/**", "colors/**" }) do
 88 |     local files = vim.fn.globpath(dir_glob, "*.lua", false, true)
 89 | 
 90 |     -- Use full paths
 91 |     files = vim.tbl_map(function(x)
 92 |       return vim.fn.fnamemodify(x, ":p")
 93 |     end, files)
 94 | 
 95 |     -- Put 'init.lua' first among files from same directory
 96 |     table.sort(files, function(a, b)
 97 |       if vim.fn.fnamemodify(a, ":h") == vim.fn.fnamemodify(b, ":h") then
 98 |         if vim.fn.fnamemodify(a, ":t") == "init.lua" then
 99 |           return true
100 |         end
101 |         if vim.fn.fnamemodify(b, ":t") == "init.lua" then
102 |           return false
103 |         end
104 |       end
105 | 
106 |       return a < b
107 |     end)
108 |     table.insert(res, files)
109 |   end
110 | 
111 |   return vim.tbl_flatten(res)
112 | end
113 | 
114 | H.default_output = function()
115 |   local cur_dir = vim.fn.fnamemodify(vim.loop.cwd(), ":t:r")
116 |   return ("doc/%s.txt"):format(cur_dir)
117 | end
118 | 
119 | -- Parsing --------------------------------------------------------------------
120 | H.lines_to_block_arr = function(lines, config)
121 |   local matched_prev, matched_cur
122 | 
123 |   local res = {}
124 |   local block_raw = { annotation = {}, section_id = {}, afterlines = {}, line_begin = 1 }
125 | 
126 |   for i, l in ipairs(lines) do
127 |     local from, to, section_id = config.annotation_extractor(l)
128 |     matched_prev, matched_cur = matched_cur, from ~= nil
129 | 
130 |     if matched_cur then
131 |       if not matched_prev then
132 |         -- Finish current block
133 |         block_raw.line_end = i - 1
134 |         table.insert(res, H.raw_block_to_block(block_raw, config))
135 | 
136 |         -- Start new block
137 |         block_raw = { annotation = {}, section_id = {}, afterlines = {}, line_begin = i }
138 |       end
139 | 
140 |       -- Add annotation line without matched annotation pattern
141 |       table.insert(block_raw.annotation, ("%s%s"):format(l:sub(0, from - 1), l:sub(to + 1)))
142 | 
143 |       -- Add section id (it is empty string in case of no section id capture)
144 |       table.insert(block_raw.section_id, section_id or "")
145 |     else
146 |       -- Add afterline
147 |       table.insert(block_raw.afterlines, l)
148 |     end
149 |   end
150 |   block_raw.line_end = #lines
151 |   table.insert(res, H.raw_block_to_block(block_raw, config))
152 | 
153 |   return res
154 | end
155 | 
156 | -- Raw block structure is an intermediate step added for convenience. It is
157 | -- a table with the following keys:
158 | -- - `annotation` - lines (after removing matched annotation pattern) that were
159 | --   parsed as annotation.
160 | -- - `section_id` - array with length equal to `annotation` length with strings
161 | --   captured as section id. Empty string of no section id was captured.
162 | -- - Everything else is used as block info (like `afterlines`, etc.).
163 | H.raw_block_to_block = function(block_raw, config)
164 |   if #block_raw.annotation == 0 and #block_raw.afterlines == 0 then
165 |     return nil
166 |   end
167 | 
168 |   local block = H.new_struct("block", {
169 |     afterlines = block_raw.afterlines,
170 |     line_begin = block_raw.line_begin,
171 |     line_end = block_raw.line_end,
172 |   })
173 |   local block_begin = block.info.line_begin
174 | 
175 |   -- Parse raw block annotation lines from top to bottom. New section starts
176 |   -- when section id is detected in that line.
177 |   local section_cur = H.new_struct(
178 |     "section",
179 |     { id = config.default_section_id, line_begin = block_begin }
180 |   )
181 | 
182 |   for i, annotation_line in ipairs(block_raw.annotation) do
183 |     local id = block_raw.section_id[i]
184 |     if id ~= "" then
185 |       -- Finish current section
186 |       if #section_cur > 0 then
187 |         section_cur.info.line_end = block_begin + i - 2
188 |         block:insert(section_cur)
189 |       end
190 | 
191 |       -- Start new section
192 |       section_cur = H.new_struct("section", { id = id, line_begin = block_begin + i - 1 })
193 |     end
194 | 
195 |     section_cur:insert(annotation_line)
196 |   end
197 | 
198 |   if #section_cur > 0 then
199 |     section_cur.info.line_end = block_begin + #block_raw.annotation - 1
200 |     block:insert(section_cur)
201 |   end
202 | 
203 |   return block
204 | end
205 | 
206 | -- Hooks ----------------------------------------------------------------------
207 | H.apply_structure_hooks = function(doc, hooks)
208 |   for _, file in ipairs(doc) do
209 |     for _, block in ipairs(file) do
210 |       hooks.block_pre(block)
211 | 
212 |       for _, section in ipairs(block) do
213 |         hooks.section_pre(section)
214 | 
215 |         local hook = hooks.sections[section.info.id]
216 |         if hook ~= nil then
217 |           hook(section)
218 |         end
219 | 
220 |         hooks.section_post(section)
221 |       end
222 | 
223 |       hooks.block_post(block)
224 |     end
225 | 
226 |     hooks.file(file)
227 |   end
228 | 
229 |   hooks.doc(doc)
230 | end
231 | 
232 | H.alias_register = function(s)
233 |   if #s == 0 then
234 |     return
235 |   end
236 | 
237 |   -- Remove first word (with bits of surrounding whitespace) while capturing it
238 |   local alias_name
239 |   s[1] = s[1]:gsub("%s*(%S+) ?", function(x)
240 |     alias_name = x
241 |     return ""
242 |   end, 1)
243 |   if alias_name == nil then
244 |     return
245 |   end
246 | 
247 |   MiniDoc.current.aliases = MiniDoc.current.aliases or {}
248 |   MiniDoc.current.aliases[alias_name] = table.concat(s, "\n")
249 | end
250 | 
251 | H.alias_replace = function(s)
252 |   if MiniDoc.current.aliases == nil then
253 |     return
254 |   end
255 | 
256 |   for i, _ in ipairs(s) do
257 |     for alias_name, alias_desc in pairs(MiniDoc.current.aliases) do
258 |       -- Escape special characters. This is done here and not while registering
259 |       -- alias to allow user to refer to aliases by its original name.
260 |       -- Store escaped words in separate variables because `vim.pesc()` returns
261 |       -- two values which might conflict if outputs are used as arguments.
262 |       local name_escaped = vim.pesc(alias_name)
263 |       local desc_escaped = vim.pesc(alias_desc)
264 |       s[i] = s[i]:gsub(name_escaped, desc_escaped)
265 |     end
266 |   end
267 | end
268 | 
269 | H.toc_register = function(s)
270 |   MiniDoc.current.toc = MiniDoc.current.toc or {}
271 |   table.insert(MiniDoc.current.toc, s)
272 | end
273 | 
274 | H.toc_insert = function(s)
275 |   if MiniDoc.current.toc == nil then
276 |     return
277 |   end
278 | 
279 |   -- Render table of contents
280 |   local toc_lines = {}
281 |   for _, toc_entry in ipairs(MiniDoc.current.toc) do
282 |     local _, tag_section = toc_entry.parent:has_descendant(function(x)
283 |       return type(x) == "table" and x.type == "section" and x.info.id == "@tag"
284 |     end)
285 |     tag_section = tag_section or {}
286 | 
287 |     local lines = {}
288 |     for i = 1, math.max(#toc_entry, #tag_section) do
289 |       local left = toc_entry[i] or ""
290 |       -- Use tag refernce instead of tag enclosure
291 |       local right = string.match(tag_section[i], "%*.*%*"):gsub("%*", "|")
292 |       -- local right = vim.trim((tag_section[i] or ""):gsub("%*", "|"))
293 |       -- Add visual line only at first entry (while not adding trailing space)
294 |       local filler = i == 1 and "." or (right == "" and "" or " ")
295 |       -- Make padding of 2 spaces at both left and right
296 |       local n_filler = math.max(74 - H.visual_text_width(left) - H.visual_text_width(right), 3)
297 |       table.insert(lines, ("  %s%s%s"):format(left, filler:rep(n_filler), right))
298 |     end
299 | 
300 |     table.insert(toc_lines, lines)
301 | 
302 |     -- Don't show `toc_entry` lines in output
303 |     toc_entry:clear_lines()
304 |   end
305 | 
306 |   for _, l in ipairs(vim.tbl_flatten(toc_lines)) do
307 |     s:insert(l)
308 |   end
309 | end
310 | 
311 | H.add_section_heading = function(s, heading)
312 |   if #s == 0 or s.type ~= "section" then
313 |     return
314 |   end
315 | 
316 |   -- Add heading
317 |   s:insert(1, ("%s~"):format(heading))
318 | end
319 | 
320 | H.enclose_var_name = function(s)
321 |   if #s == 0 or s.type ~= "section" then
322 |     return
323 |   end
324 | 
325 |   s[1] = s[1]:gsub("(%S+)", "{%1}", 1)
326 | end
327 | 
328 | ---@param init number Start of searching for first "type-like" string. It is
329 | ---   needed to not detect type early. Like in `@param a_function function`.
330 | ---@private
331 | H.enclose_type = function(s, enclosure, init)
332 |   if #s == 0 or s.type ~= "section" then
333 |     return
334 |   end
335 |   enclosure = enclosure or "`%(%1%)`"
336 |   init = init or 1
337 | 
338 |   local cur_type = H.match_first_pattern(s[1], H.pattern_sets["types"], init)
339 |   if #cur_type == 0 then
340 |     return
341 |   end
342 | 
343 |   -- Add `%S*` to front and back of found pattern to support their combination
344 |   -- with `|`. Also allows using `[]` and `?` prefixes.
345 |   local type_pattern = ("(%%S*%s%%S*)"):format(vim.pesc(cur_type[1]))
346 | 
347 |   -- Avoid replacing possible match before `init`
348 |   local l_start = s[1]:sub(1, init - 1)
349 |   local l_end = s[1]:sub(init):gsub(type_pattern, enclosure, 1)
350 |   s[1] = ("%s%s"):format(l_start, l_end)
351 | end
352 | 
353 | -- Infer data from afterlines -------------------------------------------------
354 | H.infer_header = function(b)
355 |   local has_signature = b:has_descendant(function(x)
356 |     return type(x) == "table" and x.type == "section" and x.info.id == "@signature"
357 |   end)
358 |   local has_tag = b:has_descendant(function(x)
359 |     return type(x) == "table" and x.type == "section" and x.info.id == "@tag"
360 |   end)
361 | 
362 |   if has_signature and has_tag then
363 |     return
364 |   end
365 | 
366 |   local l_all = table.concat(b.info.afterlines, " ")
367 |   local tag, signature
368 | 
369 |   -- Try function definition
370 |   local fun_capture = H.match_first_pattern(l_all, H.pattern_sets["afterline_fundef"])
371 |   if #fun_capture > 0 then
372 |     tag = tag or ("%s()"):format(fun_capture[1])
373 |     signature = signature or ("%s%s"):format(fun_capture[1], fun_capture[2])
374 |   end
375 | 
376 |   -- Try general assignment
377 |   local assign_capture = H.match_first_pattern(l_all, H.pattern_sets["afterline_assign"])
378 |   if #assign_capture > 0 then
379 |     tag = tag or assign_capture[1]
380 |     signature = signature or assign_capture[1]
381 |   end
382 | 
383 |   if tag ~= nil then
384 |     -- First insert signature (so that it will appear after tag section)
385 |     if not has_signature then
386 |       b:insert(1, H.as_struct({ signature }, "section", { id = "@signature" }))
387 |     end
388 | 
389 |     -- Insert tag
390 |     if not has_tag then
391 |       b:insert(1, H.as_struct({ tag }, "section", { id = "@tag" }))
392 |     end
393 |   end
394 | end
395 | 
396 | function H.is_module(name)
397 |   if string.find(name, "%(") then
398 |     return false
399 |   end
400 |   if string.find(name, "[A-Z]") then
401 |     return false
402 |   end
403 |   return true
404 | end
405 | 
406 | H.format_signature = function(line)
407 |   -- Try capture function signature
408 |   local name, args = line:match("(%S-)(%b())")
409 | 
410 | 
411 |   -- Otherwise pick first word
412 |   name = name or line:match("(%S+)")
413 |   if not args and H.is_module(name) then
414 |     return ""
415 |   end
416 |   local name_elems = vim.split(name, ".", { plain = true })
417 |   name = name_elems[#name_elems]
418 | 
419 |   if not name then
420 |     return ""
421 |   end
422 | 
423 |   -- Tidy arguments
424 |   if args and args ~= "()" then
425 |     local arg_parts = vim.split(args:sub(2, -2), ",")
426 |     local arg_list = {}
427 |     for _, a in ipairs(arg_parts) do
428 |       -- Enclose argument in `{}` while controlling whitespace
429 |       table.insert(arg_list, ("{%s}"):format(vim.trim(a)))
430 |     end
431 |     args = ("(%s)"):format(table.concat(arg_list, ", "))
432 |   end
433 | 
434 |   return ("`%s`%s"):format(name, args or "")
435 | end
436 | 
437 | -- Work with structures -------------------------------------------------------
438 | -- Constructor
439 | H.new_struct = function(struct_type, info)
440 |   local output = {
441 |     info = info or {},
442 |     type = struct_type,
443 |   }
444 | 
445 |   output.insert = function(self, index, child)
446 |     -- Allow both `x:insert(child)` and `x:insert(1, child)`
447 |     if child == nil then
448 |       child, index = index, #self + 1
449 |     end
450 | 
451 |     if type(child) == "table" then
452 |       child.parent = self
453 |       child.parent_index = index
454 |     end
455 | 
456 |     table.insert(self, index, child)
457 | 
458 |     H.sync_parent_index(self)
459 |   end
460 | 
461 |   output.remove = function(self, index)
462 |     index = index or #self
463 |     table.remove(self, index)
464 | 
465 |     H.sync_parent_index(self)
466 |   end
467 | 
468 |   output.has_descendant = function(self, predicate)
469 |     local bool_res, descendant = false, nil
470 |     H.apply_recursively(function(x)
471 |       if not bool_res and predicate(x) then
472 |         bool_res = true
473 |         descendant = x
474 |       end
475 |     end, self)
476 |     return bool_res, descendant
477 |   end
478 | 
479 |   output.has_lines = function(self)
480 |     return self:has_descendant(function(x)
481 |       return type(x) == "string"
482 |     end)
483 |   end
484 | 
485 |   output.clear_lines = function(self)
486 |     for i, x in ipairs(self) do
487 |       if type(x) == "string" then
488 |         self[i] = nil
489 |       else
490 |         x:clear_lines()
491 |       end
492 |     end
493 |   end
494 | 
495 |   return output
496 | end
497 | 
498 | H.sync_parent_index = function(x)
499 |   for i, _ in ipairs(x) do
500 |     if type(x[i]) == "table" then
501 |       x[i].parent_index = i
502 |     end
503 |   end
504 |   return x
505 | end
506 | 
507 | -- Converter (this ensures that children have proper parent-related data)
508 | H.as_struct = function(array, struct_type, info)
509 |   -- Make default info `info` for cases when structure is created manually
510 |   local default_info = ({
511 |     section = { id = "@text", line_begin = -1, line_end = -1 },
512 |     block = { afterlines = {}, line_begin = -1, line_end = -1 },
513 |     file = { path = "" },
514 |     doc = { input = {}, output = "", config = H.get_config() },
515 |   })[struct_type]
516 |   info = vim.tbl_deep_extend("force", default_info, info or {})
517 | 
518 |   local res = H.new_struct(struct_type, info)
519 |   for _, x in ipairs(array) do
520 |     res:insert(x)
521 |   end
522 |   return res
523 | end
524 | 
525 | -- Work with text -------------------------------------------------------------
526 | H.ensure_indent = function(text, n_indent_target)
527 |   local lines = vim.split(text, "\n")
528 |   local n_indent, n_indent_cur = math.huge, math.huge
529 | 
530 |   -- Find number of characters in indent
531 |   for _, l in ipairs(lines) do
532 |     -- Update lines indent: minimum of all indents except empty lines
533 |     if n_indent > 0 then
534 |       _, n_indent_cur = l:find("^%s*")
535 |       -- Condition "current n-indent equals line length" detects empty line
536 |       if (n_indent_cur < n_indent) and (n_indent_cur < l:len()) then
537 |         n_indent = n_indent_cur
538 |       end
539 |     end
540 |   end
541 | 
542 |   -- Ensure indent
543 |   local indent = string.rep(" ", n_indent_target)
544 |   for i, l in ipairs(lines) do
545 |     if l ~= "" then
546 |       lines[i] = indent .. l:sub(n_indent + 1)
547 |     end
548 |   end
549 | 
550 |   return table.concat(lines, "\n")
551 | end
552 | 
553 | H.align_text = function(text, width, direction)
554 |   if type(text) ~= "string" then
555 |     return
556 |   end
557 |   text = vim.trim(text)
558 |   width = width or 78
559 |   direction = direction or "left"
560 | 
561 |   -- Don't do anything if aligning left or line is a whitespace
562 |   if direction == "left" or text:find("^%s*
quot;) then
563 |     return text
564 |   end
565 | 
566 |   local n_left = math.max(0, 78 - H.visual_text_width(text))
567 |   if direction == "center" then
568 |     n_left = math.floor(0.5 * n_left)
569 |   end
570 | 
571 |   return (" "):rep(n_left) .. text
572 | end
573 | 
574 | H.visual_text_width = function(text)
575 |   -- Ignore concealed characters (usually "invisible" in 'help' filetype)
576 |   local _, n_concealed_chars = text:gsub("([*|`])", "%1")
577 |   return vim.fn.strdisplaywidth(text) - n_concealed_chars
578 | end
579 | 
580 | --- Return earliest match among many patterns
581 | ---
582 | --- Logic here is to test among several patterns. If several got a match,
583 | --- return one with earliest match.
584 | ---
585 | ---@private
586 | H.match_first_pattern = function(text, pattern_set, init)
587 |   local start_tbl = vim.tbl_map(function(pattern)
588 |     return text:find(pattern, init) or math.huge
589 |   end, pattern_set)
590 | 
591 |   local min_start, min_id = math.huge, nil
592 |   for id, st in ipairs(start_tbl) do
593 |     if st < min_start then
594 |       min_start, min_id = st, id
595 |     end
596 |   end
597 | 
598 |   if min_id == nil then
599 |     return {}
600 |   end
601 |   return { text:match(pattern_set[min_id], init) }
602 | end
603 | 
604 | -- Utilities ------------------------------------------------------------------
605 | H.apply_recursively = function(f, x, used)
606 |   used = used or {}
607 |   if used[x] then
608 |     return
609 |   end
610 |   f(x)
611 |   used[x] = true
612 | 
613 |   if type(x) == "table" then
614 |     for _, t in ipairs(x) do
615 |       H.apply_recursively(f, t, used)
616 |     end
617 |   end
618 | end
619 | 
620 | H.collect_strings = function(x)
621 |   local res = {}
622 |   H.apply_recursively(function(y)
623 |     if type(y) == "string" then
624 |       -- Allow `\n` in strings
625 |       table.insert(res, vim.split(y, "\n"))
626 |     end
627 |   end, x)
628 |   -- Flatten to only have strings and not table of strings (from `vim.split`)
629 |   return vim.tbl_flatten(res)
630 | end
631 | 
632 | H.file_read = function(path)
633 |   local file = assert(io.open(path))
634 |   local contents = file:read("*all")
635 |   file:close()
636 | 
637 |   return vim.split(contents, "\n")
638 | end
639 | 
640 | H.file_write = function(path, lines)
641 |   -- Ensure target directory exists
642 |   local dir = vim.fn.fnamemodify(path, ":h")
643 |   vim.fn.mkdir(dir, "p")
644 | 
645 |   -- Write to file
646 |   vim.fn.writefile(lines, path, "b")
647 | end
648 | 
649 | H.full_path = function(path)
650 |   return vim.fn.resolve(vim.fn.fnamemodify(path, ":p"))
651 | end
652 | 
653 | H.message = function(msg)
654 |   vim.cmd("echomsg " .. vim.inspect("(mini.doc) " .. msg))
655 | end
656 | 
657 | minidoc.setup({})
658 | minidoc.generate(
659 |   {
660 |     "./lua/notify/init.lua",
661 |     "./lua/notify/config/init.lua",
662 |     "./lua/notify/render/init.lua",
663 |   },
664 |   nil,
665 |   {
666 |     hooks = vim.tbl_extend("force", minidoc.default_hooks, {
667 |       block_post = function(b)
668 |         if not b:has_lines() then return end
669 | 
670 |         local found_param, found_field = false, false
671 |         local n_tag_sections = 0
672 |         H.apply_recursively(function(x)
673 |           if not (type(x) == 'table' and x.type == 'section') then return end
674 | 
675 |           -- Add headings before first occurence of a section which type usually
676 |           -- appear several times
677 |           if not found_param and x.info.id == '@param' then
678 |             H.add_section_heading(x, 'Parameters')
679 |             found_param = true
680 |           end
681 |           if not found_field and x.info.id == '@field' then
682 |             H.add_section_heading(x, 'Fields')
683 |             found_field = true
684 |           end
685 | 
686 |           if x.info.id == '@tag' then
687 |             local text = x[1]
688 |             local tag = string.match(text, "%*.*%*")
689 |             local prefix = (string.sub(tag, 2, #tag - 1))
690 |             if not H.is_module(prefix) then
691 |               prefix = ""
692 |             end
693 |             local n_filler = math.max(78 - H.visual_text_width(prefix) - H.visual_text_width(tag), 3)
694 |             local line = ("%s%s%s"):format(prefix, (" "):rep(n_filler), tag)
695 |             x:remove(1)
696 |             x:insert(1, line)
697 |             x.parent:remove(x.parent_index)
698 |             n_tag_sections = n_tag_sections + 1
699 |             x.parent:insert(n_tag_sections, x)
700 |           end
701 |         end, b)
702 | 
703 |         -- b:insert(1, H.as_struct({ string.rep('=', 78) }, 'section'))
704 |         b:insert(H.as_struct({ '' }, 'section'))
705 |       end,
706 | 
707 | 
708 |       doc = function(d)
709 |         -- Render table of contents
710 |         H.apply_recursively(function(x)
711 |           if not (type(x) == 'table' and x.type == 'section' and x.info.id == '@toc') then return end
712 |           H.toc_insert(x)
713 |         end, d)
714 | 
715 |         -- Insert modeline
716 |         d:insert(
717 |           H.as_struct(
718 |             { H.as_struct({ H.as_struct({ ' vim:tw=78:ts=8:noet:ft=help:norl:' }, 'section') }, 'block') },
719 |             'file'
720 |           )
721 |         )
722 |       end,
723 |       sections = {
724 |         ['@generic'] = function(s)
725 |           s:remove(1)
726 |         end,
727 |         ['@field'] = function(s)
728 |           -- H.mark_optional(s)
729 |           if string.find(s[1], "^private ") then
730 |             s:remove(1)
731 |             return
732 |           end
733 |           H.enclose_var_name(s)
734 |           H.enclose_type(s, '`%(%1%)`', s[1]:find('%s'))
735 |         end,
736 |         ['@alias'] = function(s)
737 |           local name = s[1]:match('%s*(%S*)')
738 |           local alias = s[1]:match('%s(.*)
#39;)
739 |           s[1] = ("`%s` → `%s`"):format(name, alias)
740 |           H.add_section_heading(s, 'Alias')
741 |           s:insert(1, H.as_struct({ ("*%s*"):format(name) }, "section", { id = "@tag" }))
742 |         end,
743 | 
744 |         ['@param'] = function(s)
745 |           H.enclose_var_name(s)
746 |           H.enclose_type(s, '`%(%1%)`', s[1]:find('%s'))
747 |         end,
748 |         ['@return'] = function(s)
749 |           H.enclose_type(s, '`%(%1%)`', 1)
750 |           H.add_section_heading(s, 'Return')
751 |         end,
752 |         ['@nodoc'] = function(s) s.parent:clear_lines() end,
753 |         ['@class'] = function(s)
754 |           H.enclose_var_name(s)
755 |           -- Add heading
756 |           local line = s[1]
757 |           s:remove(1)
758 |           local class_name = string.match(line, "%{(.*)%}")
759 |           local inherits = string.match(line, ": (.*)")
760 |           if inherits then
761 |             s:insert(1, ("Inherits: `%s`"):format(inherits))
762 |             s:insert(2, "")
763 |           end
764 |           s:insert(1, H.as_struct({ ("*%s*"):format(class_name) }, "section", { id = "@tag" }))
765 |         end,
766 | 
767 |         ['@signature'] = function(s)
768 |           s[1] = H.format_signature(s[1])
769 |           if s[1] ~= "" then
770 |             table.insert(s, "")
771 |           end
772 |         end,
773 | 
774 |       },
775 | 
776 |       file = function(f)
777 |         if not f:has_lines() then
778 |           return
779 |         end
780 | 
781 |         if f.info.path ~= "./lua/notify/init.lua" then
782 |           f:insert(1, H.as_struct({ H.as_struct({ string.rep("=", 78) }, "section") }, "block"))
783 |           f:insert(H.as_struct({ H.as_struct({ "" }, "section") }, "block"))
784 |         else
785 |           f:insert(
786 |             1,
787 |             H.as_struct(
788 |               {
789 |                 H.as_struct(
790 |                   { "*nvim-notify.txt*   A fancy, configurable notification manager for NeoVim" },
791 |                   "section"
792 |                 ),
793 |               },
794 |               "block"
795 |             )
796 |           )
797 |           f:insert(2, H.as_struct({ H.as_struct({ "" }, "section") }, "block"))
798 |           f:insert(3, H.as_struct({ H.as_struct({ string.rep("=", 78) }, "section") }, "block"))
799 |           f:insert(H.as_struct({ H.as_struct({ "" }, "section") }, "block"))
800 |         end
801 |       end,
802 |     }),
803 |   }
804 | )
805 | 


--------------------------------------------------------------------------------
/scripts/style:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | 
3 | stylua lua tests
4 | 


--------------------------------------------------------------------------------
/scripts/test:
--------------------------------------------------------------------------------
 1 | #!/bin/bash
 2 | tempfile=".test_output.tmp"
 3 | 
 4 | if [[ -n $1 ]]; then
 5 | 	nvim --headless --noplugin -u tests/init.vim -c "PlenaryBustedFile $1" | tee "${tempfile}"
 6 | else
 7 | 	nvim --headless --noplugin -u tests/init.vim -c "PlenaryBustedDirectory tests/ {minimal_init = 'tests/init.vim'}" | tee "${tempfile}"
 8 | fi
 9 | 
10 | # Plenary doesn't emit exit code 1 when tests have errors during setup
11 | errors=$(sed 's/\x1b\[[0-9;]*m//g' "${tempfile}" | awk '/(Errors|Failed) :/ {print $3}' | grep -v '0')
12 | 
13 | rm "${tempfile}"
14 | 
15 | if [[ -n $errors ]]; then
16 |   echo "Tests failed"
17 |   exit 1
18 | fi
19 | 
20 | exit 0
21 | 


--------------------------------------------------------------------------------
/stylua.toml:
--------------------------------------------------------------------------------
1 | column_width = 100
2 | line_endings = "Unix"
3 | indent_type = "Spaces"
4 | indent_width = 2
5 | quote_style = "AutoPreferDouble"
6 | 


--------------------------------------------------------------------------------
/tests/init.vim:
--------------------------------------------------------------------------------
1 | set rtp+=.
2 | set rtp+=../plenary.nvim
3 | set termguicolors
4 | runtime! plugin/plenary.vim
5 | 


--------------------------------------------------------------------------------
/tests/manual/merge_duplicates.lua:
--------------------------------------------------------------------------------
 1 | local function coroutine_resume()
 2 |   local co = assert(coroutine.running())
 3 |   return function(...)
 4 |     local ret = { coroutine.resume(co, ...) }
 5 |     -- Re-raise errors with correct traceback
 6 |     local ok, err = unpack(ret)
 7 |     if not ok then
 8 |       error(debug.traceback(co, err))
 9 |     end
10 |     return unpack(ret, 2)
11 |   end
12 | end
13 | 
14 | local function coroutine_sleep()
15 |   local resume = coroutine_resume()
16 |   return function(ms)
17 |     vim.defer_fn(resume, ms)
18 |     coroutine.yield()
19 |   end
20 | end
21 | 
22 | local function run()
23 |   local sleep = coroutine_sleep()
24 | 
25 |   local countdown = function(seconds)
26 |     local id
27 |     for i = 1, seconds do
28 |       -- local msg = string.format('Will show once again in %d seconds', seconds - i + 1)
29 |       local msg = string.format("Will show once again in %d seconds", seconds - i + 1)
30 |       id = vim.notify(msg, vim.log.levels.WARN, { replace = id }).id
31 |       sleep(1000)
32 |     end
33 |   end
34 | 
35 |   local texts = {
36 |     "AAA This is first text",
37 |     "BBBBBB Second text of duplicate notifications",
38 |     -- 'CCCCCCCCC Third text',
39 |   }
40 | 
41 |   local show_all = function(level)
42 |     for _, text in ipairs(texts) do
43 |       vim.notify(text, level)
44 |       sleep(50)
45 |     end
46 |   end
47 | 
48 |   show_all()
49 | 
50 |   sleep(1000)
51 |   show_all()
52 | 
53 |   sleep(1000)
54 |   show_all()
55 | 
56 |   sleep(1000)
57 |   show_all(vim.log.levels.WARN)
58 |   sleep(1000)
59 |   show_all(vim.log.levels.WARN)
60 | 
61 |   sleep(1000)
62 |   show_all()
63 | 
64 |   -- wait until the previous notifications disappear
65 |   countdown(10)
66 |   show_all()
67 | 
68 |   sleep(2000)
69 |   for _ = 1, 41 do
70 |     sleep(50)
71 |     show_all()
72 |   end
73 | end
74 | 
75 | coroutine.wrap(run)()
76 | 


--------------------------------------------------------------------------------
/tests/unit/init_spec.lua:
--------------------------------------------------------------------------------
  1 | local async = require("plenary.async")
  2 | async.tests.add_to_env()
  3 | vim.opt.termguicolors = true
  4 | A = vim.schedule_wrap(function(...)
  5 |   print(vim.inspect(...))
  6 | end)
  7 | 
  8 | describe("checking public interface", function()
  9 |   local notify = require("notify")
 10 |   local async_notify = require("notify").async
 11 |   assert:add_formatter(vim.inspect)
 12 | 
 13 |   before_each(function()
 14 |     notify.setup({ background_colour = "#000000" })
 15 |     notify.dismiss({ pending = true, silent = true })
 16 |   end)
 17 | 
 18 |   describe("notifications", function()
 19 |     it("returns all previous notifications", function()
 20 |       notify.notify("test", "error")
 21 |       local notifs = notify.history()
 22 |       assert.are.same({
 23 |         {
 24 |           icon = "",
 25 |           id = 1,
 26 |           level = "ERROR",
 27 |           message = { "test" },
 28 |           render = notifs[1].render,
 29 |           time = notifs[1].time,
 30 |           title = { "", notifs[1].title[2] },
 31 |         },
 32 |       }, notifs)
 33 |     end)
 34 | 
 35 |     describe("rendering", function()
 36 |       a.it("uses custom render in config", function()
 37 |         local called = false
 38 |         notify.setup({
 39 |           background_colour = "#000000",
 40 |           render = function()
 41 |             called = true
 42 |           end,
 43 |         })
 44 |         notify.async("test", "error").events.open()
 45 |         assert.is.True(called)
 46 |       end)
 47 | 
 48 |       a.it("validates max width and prefix length", function()
 49 |         local terminal_width = vim.o.columns
 50 |         notify.setup({
 51 |           background_colour = "#000000",
 52 |           max_width = function()
 53 |             return math.min(terminal_width, 50)
 54 |           end,
 55 |         })
 56 | 
 57 |         local win = notify.async("test", "info").events.open()
 58 | 
 59 |         assert.is.True(vim.api.nvim_win_get_width(win) <= terminal_width)
 60 | 
 61 |         local notif = notify.notify("Test Notification", "info", {
 62 |           title = "Long Title That Should Be Cut Off",
 63 |         })
 64 | 
 65 |         local prefix_title = notif.title and notif.title[1] or "Default Title"
 66 | 
 67 |         local prefix_length = vim.str_utfindex(prefix_title)
 68 |         assert.is.True(prefix_length <= terminal_width)
 69 |       end)
 70 | 
 71 |       a.it("uses custom render in call", function()
 72 |         local called = false
 73 |         notify
 74 |           .async("test", "error", {
 75 |             render = function()
 76 |               called = true
 77 |             end,
 78 |           }).events
 79 |           .open()
 80 |         assert.is.True(called)
 81 |       end)
 82 |     end)
 83 | 
 84 |     describe("replacing", function()
 85 |       it("inherits options", function()
 86 |         local orig = notify.notify("first", "info", { title = "test", icon = "x" })
 87 |         local next = notify.notify("second", nil, { replace = orig })
 88 | 
 89 |         assert.are.same(
 90 |           next,
 91 |           vim.tbl_extend("force", orig, { id = next.id, message = next.message })
 92 |         )
 93 |       end)
 94 | 
 95 |       a.it("uses same window", function()
 96 |         local orig = async_notify("first", "info", { timeout = false })
 97 |         local win = orig.events.open()
 98 |         async_notify("second", nil, { replace = orig, timeout = 100 })
 99 |         async.util.scheduler()
100 |         local found = false
101 |         local bufs = vim.api.nvim_list_bufs()
102 |         for _, buf in ipairs(bufs) do
103 |           if vim.api.nvim_buf_get_lines(buf, 0, -1, false)[1] == "second" then
104 |             assert.Not(found)
105 |             assert.same(vim.fn.bufwinid(buf), win)
106 |             found = true
107 |           end
108 |         end
109 |       end)
110 |     end)
111 |   end)
112 | 
113 |   a.it("uses the configured minimum width", function()
114 |     notify.setup({
115 |       background_colour = "#000000",
116 |       minimum_width = 20,
117 |     })
118 |     local win = notify.async("test").events.open()
119 |     assert.equal(vim.api.nvim_win_get_width(win), 20)
120 |   end)
121 | 
122 |   a.it("uses the configured max width", function()
123 |     notify.setup({
124 |       background_colour = "#000000",
125 |       max_width = function()
126 |         return 3
127 |       end,
128 |     })
129 |     local win = notify.async("test").events.open()
130 |     assert.equal(vim.api.nvim_win_get_width(win), 3)
131 |   end)
132 | 
133 |   a.it("uses the configured max height", function()
134 |     local instance = notify.instance({
135 |       background_colour = "#000000",
136 |       max_height = function()
137 |         return 3
138 |       end,
139 |     }, false)
140 |     local win = instance.async("test").events.open()
141 |     assert.equal(vim.api.nvim_win_get_height(win), 3)
142 |   end)
143 | 
144 |   a.it("renders title as longest line", function()
145 |     local instance = notify.instance({
146 |       background_colour = "#000000",
147 |       minimum_width = 10,
148 |     }, false)
149 |     local win = instance.async("test", nil, { title = { string.rep("a", 16), "" } }).events.open()
150 |     assert.equal(21, vim.api.nvim_win_get_width(win))
151 |   end)
152 | 
153 |   a.it("renders notification above config level", function()
154 |     local win =
155 |       notify.async("test", "info", { message = { string.rep("a", 16), "" } }).events.open()
156 |     assert.Not.Nil(vim.api.nvim_win_get_config(win))
157 |   end)
158 | 
159 |   a.it("doesn't render notification below config level", function()
160 |     async.run(function()
161 |       local notif = notify.async("test", "debug", { message = { string.rep("a", 16), "" } })
162 |       local win = notif.events.open()
163 |       async.api.nvim_buf_set_option(async.api.nvim_win_get_buf(win), "filetype", "test")
164 |     end)
165 |     async.util.sleep(100)
166 |     local bufs = vim.api.nvim_list_bufs()
167 |     for _, buf in ipairs(bufs) do
168 |       assert.Not.same(vim.api.nvim_buf_get_option(buf, "filetype"), "test")
169 |     end
170 |   end)
171 |   a.it("refreshes timeout on replace", function()
172 |     -- Don't want to spend time animating
173 |     notify.setup({ background_colour = "#000000", stages = "static" })
174 | 
175 |     local notif = notify.async("test", "error", { timeout = 500 })
176 |     local win = notif.events.open()
177 |     a.util.sleep(300)
178 |     notify.async("test2", "error", { replace = notif })
179 |     a.util.sleep(300)
180 |     a.util.scheduler()
181 |     assert(vim.api.nvim_win_is_valid(win))
182 |   end)
183 | 
184 |   describe("util", function()
185 |     local util = require("notify.util")
186 | 
187 |     describe("max_line_width()", function()
188 |       it("returns the maximal width of a table of lines", function()
189 |         assert.equals(5, util.max_line_width({ "12", "12345", "123" }))
190 |       end)
191 | 
192 |       it("returns 0 for nil input", function()
193 |         assert.equals(0, util.max_line_width())
194 |       end)
195 | 
196 |       describe("notification width", function()
197 |         a.it("handles multibyte characters correctly", function()
198 |           local instance = notify.instance({
199 |             background_colour = "#000000",
200 |             minimum_width = 1,
201 |             render = "minimal",
202 |           }, false)
203 |           local win = instance.async("\u{1D4AF}\u{212F}\u{1D4C8}\u{1D4C9}").events.open() -- "𝒯ℯ𝓈𝓉"
204 |           assert.equal(4, vim.api.nvim_win_get_width(win))
205 |         end)
206 | 
207 |         a.it("handles combining character sequences correctly", function()
208 |           local instance = notify.instance({
209 |             background_colour = "#000000",
210 |             minimum_width = 1,
211 |             render = "minimal",
212 |           }, false)
213 |           local win = instance
214 |             .async(
215 |               "T\u{0336}\u{0311}\u{0349}"
216 |                 .. "e\u{0336}\u{030E}\u{0332}"
217 |                 .. "s\u{0334}\u{0301}\u{0329}"
218 |                 .. "t\u{0337}\u{0301}\u{031C}" -- "T̶͉̑e̶̲̎ś̴̩t̷̜́"
219 |             ).events
220 |             .open()
221 |           assert.equal(4, vim.api.nvim_win_get_width(win))
222 |         end)
223 | 
224 |         a.it("respects East Asian Width Class", function()
225 |           local instance = notify.instance({
226 |             background_colour = "#000000",
227 |             minimum_width = 1,
228 |             render = "minimal",
229 |           }, false)
230 |           local win = instance.async("\u{FF34}\u{FF45}\u{FF53}\u{FF54}").events.open() -- "Test"
231 |           assert.equal(8, vim.api.nvim_win_get_width(win))
232 |         end)
233 |       end)
234 |     end)
235 |   end)
236 | end)
237 | 


--------------------------------------------------------------------------------