refactor: rework notification renderer and poller#302
Open
nqrk wants to merge 38 commits intoj-hui:mainfrom
Open
refactor: rework notification renderer and poller#302nqrk wants to merge 38 commits intoj-hui:mainfrom
nqrk wants to merge 38 commits intoj-hui:mainfrom
Conversation
Cache group separator, header and messages to reduce rendering work.
The renderer now reuse previous tokens when:
- window was not resized (width)
- same item count, icon or group name
- tokens did not reach ttl expiration (see model.tick)
Lines are now rendered using array of words composed of only non-space characters. Spacing is added on the fly by calculating the start and the end position of each words. Fidget now automatically reflows lines that exceed max_width via word-break. Annotes indentation is set by default to "annote" (see options.align) This is the first step toward introducing tree-sitter highlight inside the renderer.
Related to j-hui#159 Implements tree-sitter highlight to rendered tokens.
Default highlight is set to "markdown_inline".
Toggle default notification highlight:
- notification.view.highlight = string|false
Hide concealed markdown tags:
- notification.view.hide_conceal = boolean
The highlight function can now reuse previously generated hl tokens.
Reorganise view render to return a "Notification". This is the first step to introduce per-message options via notify.
Related to j-hui#244 Position of the text inside the window can be changed with: - notification.view.text_position = "left"|"right" If in future change, we want to implement different window position or a double notification window (by duplicating states), each message can be aligned independently under "eol" or "eol_right_align".
Related to j-hui#244 A notification can now have different visual properties. This apply only to text inside the window, for now only one window is supported. Users can now let each message be independently aligned to the left or right: - `vim.notify([[block of code]], nil, { position = "left" })` Message header (group name, sep etc) takes value from config instead.
When editor size is cached and a notification is draw while "window.max_width" value changes, the renderer still uses the previously cached width and lays out the lines based on a wrong value. Lines would overflow when message position is set to "left" and max_width suddenly change without editor size changing.
Users can now let each message be independently highlighted with different language:
- `vim.notify([[block of code]], nil, { lang = "lua" })`
Cached items are now properly dereferenced when unused. When a notification reaches its ttl expiration it is removed from the cache. When the notification window closes, all items (including headers) are cleared from the cache except for the "default" group header and separator. + minor improvement with rehashing and table allocation inside the render loop.
Cache "highlights" queries and reuse them when parsing the same language. + minor improvement in the duplicate items detection loop.
Uses nvim "right_align" text position instead of computing padding ourselves.
Integrate with external plugins when loading window config.
Poller now allocates one timer and manages it throughout the entire fidget lifecycle. Timer resources are freed after each reset of the notification subsystem state.
Polls frame time using high-resolution timestamps (in nanoseconds). Converts "item.last_updated" to unix time before storing item to history buffer.
Removes repeated allocations caused by frequent accesses to editor column value. Now updates cached columns only during window resize events, this reduces memory usage inside the rendering loop (see view.window_max)
Add a lock to skip rendering when the state remains constant.
A constant state occurs when:
- No notification changes (in groups)
- Window id and dimensions remain unchanged
Now we render only when necessary and reduces the memory usage
during frequent polling with small poll rates.
This allows notification or progress to properly close the window
and clean up resources whenever an error occurs in the polling logic.
To control whether errors should be displayed or logged silently:
- notification.options.show_errors = boolean
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This pull request primarily refactors the rendering pipeline and includes changes to the notification poller.
Main focus is to bring up new features while trying to reduces memory usage.
Noticeable changes for users
resetcommand to clear notification state and free resources.update_hookis set tofalse.positionandlangoverride as notification options.How to test?
Load this PR either manually or by using Lazy
{ "nqrk/fidget.nvim", branch = "feat-renderv2", opts = { notification = { override_vim_notify = true } } }If you find any issues, please report them here. It would be helpful if you could include relevant
context about the issue or provide a way to reproduce it using a test case. (see
notify.lua)Changes in the background
Cache
Fidget now caches the group separator, header and messages to reduce rendering work.
When
model.tick()runs and a notification item reaches ttl, the item is removed from the cache.When the notification closes, all items (including headers) are cleared from the cache except for
the
defaultgroup header and separator (since they never change).Editor columns is also cached, and the value is updated only during window resize events.
Treesitter queries to the
highlights.scmfile are also cached per language.:lua = require("fidget.notification.model").cache{ group_header = { Notifications = { { hdr = { <1>{ " ", { "FidgetNoBlend" } }, { "Notifications", { "FidgetNoBlend", "Title" } }, { " ", { "FidgetNoBlend" } }, { "❰❰", { "FidgetNoBlend", "Special" } }, <table 1> } }, 18, "❰❰" } }, group_sep = {}, render_item = {}, render_width = 306, -- editor width window = 1000 -- window id }When
render_itemis used to display a notification.History
If the
update_hookfunction (used for item deduplication) is not set tofalse, a removed duplicate item will not be added to the history buffer.Tokenization
The tokenization of message has changed: lines are now rendered using an array of words composed of
only non-space characters. Spacing is added during rendering by calculating the start and end
positions of each word.
max_widthare reflowed in a word-break fashion.view.options.align)window.options.tabstopvalue.window.options.relative)Highlights
Notifications can now be highlighted via Treesitter.
markdown_inline.For certain types of languages such as Markdown, it is aesthetically pleasing to remove tags.
The renderer can do this by default when the tags highlight group is set to
@concealand theglobal config option
view.hide_concealis set totrue.The highlighter creates the same structure used by the tokenizer: including start and end positions
of the highlight ranges.
{ srow = integer -- starting row (to sync with tokens) scol = integer -- starting column ecol = integer -- end column text = string -- character or word hl = integer -- highlight group (from hlID) }Renderer have to synchronizes those values with the current tokenized message, taking into account
extra lines added when a line overflows and has to be reflowed.
Renderer
The
render()method insideview.luareturns a unified message object to the poller callback.{ rows = integer -- total amount of lines in the message lines = tokens -- see render_items() bellow width = integer -- width of the longest line inside the window opts = { upwards = boolean -- config: display from bottom to top position = string -- config: default virtual text position to use } }Returned by
render_items()is atokens.{ line = NotificationItem[] -- the message tokenized including highlights opts = { position = string -- per-line positioning ... } }State lock
A state lock was added to skip rendering when the state remains constant.
The lock works as follows:
vim.notify()is called and message is passed tonotify().true.window.set_linesis called if rendering lock istrue.false.A state change will set the rendering lock to
true(in an "update" status) and thenotification is rendered again on the next frame. (see
poll_rate)Fidget now only renders when necessary. This leads to significant drop in memory usage during
frequent polling since we also reduce the number of allocations required for calling nvim internal
api.
Poller
The poller now allocates one timer and manages it for the entire lifecycle instead of stopping,
closing and dereferencing a timer at each poll. The resources are freed after a reset of the
notification subsystem state.
In case of errors:
options.show_errors)Frame time
Now uses libuv to handle time-based events, frame time is calculated from the difference between
high-resolution timestamp values in nanoseconds. (see
uv.hrtime)When an item is added to the history buffer,
item.last_updatedis cast to a unix-time format (seeuv.clock_gettime)Perf. / misc.
vim.tbl_methods in favor ofvim.iter.window.set_lines()allow us to drop the legacy padding code that wasneeded to support
nvim < 0.11. So instead of computing the padding ourselves we can use the nativevirt_text_pos = right_align.window.guardmethod can now receive arguments to pass to thepcallfunction, we skip theneed for closures to call it. Also the list of errors as a string table is allocated only if
pcallreturns an error.
get_window_position().Instead, when loading the window module configuration, the integration module is iterated over to check for
options.enablebooleans.notify.luawas added to the tests folder. It shows multiple notificationscenarios through
vim.notifywith different configuration options. It is not an automatedheadless test but a visual guideline for writing or testing the rendering code. Feel free to
create a better one with Plenary etc, i don't have it in me to do it.