Skip to content

Commit

Permalink
refactor(notification): consolidate notification state into single type
Browse files Browse the repository at this point in the history
Doing so simplifies passing around state between notification.lua (which
owns the state) and notification/model.lua (which manages/modifies the
state). It also means that model.lua can directly modify state, rather
using a weird ad hoc API to get around levels of indirection.

This commit primarily exists as a snapshot before I introduce support
for maintaining notification history.
  • Loading branch information
j-hui committed Dec 10, 2023
1 parent df0caf2 commit 1ad95a2
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 52 deletions.
54 changes: 32 additions & 22 deletions lua/fidget/notification.lua
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,19 @@ local logger = require("fidget.logger")
---@field error_annote string|nil Default annotation for error items
---@field priority number|nil Order in which group should be displayed; defaults to `50`

--- Notification element containing a message and optional annotation.
---
---@class Item
---@field key Key Used to distinguish this item from others
---@field message string Displayed message for the item
---@field annote string|nil Optional title that accompanies the message
---@field style string Style used to render the annote/title, if any
---@field hidden boolean Whether this item should be shown
---@field expires_at number What time this item should be removed; math.huge means never
---@field last_updated number What time this item was last updated
---@field removed true|nil Whether this item is deleted
---@field data any|nil Arbitrary data attached to notification item

--- Default notification configuration.
---
--- Exposed publicly because it might be useful for users to integrate for when
Expand Down Expand Up @@ -148,12 +161,13 @@ require("fidget.options").declare(notification, "notification", notification.opt
end
end)

--- The "model" of notifications: a list of notification groups.
---@type Group[]
local groups = {}
--- The "model" (abstract state) of notifications.
---@type State
local state = {
groups = {},
view_suppressed = false,
}

--- Whether the notification window is suppressed.
local view_suppressed = false

--- Send a notification to the Fidget notifications subsystem.
---
Expand Down Expand Up @@ -185,11 +199,7 @@ function notification.notify(msg, level, opts)
end

local now = poll.get_time()
local n_groups = #groups
notification.model.update(now, notification.options.configs, groups, msg, level, opts)
if n_groups ~= #groups then
groups = vim.fn.sort(groups, function(a, b) return (a.config.priority or 50) - (b.config.priority or 50) end)
end
notification.model.update(now, notification.options.configs, state, msg, level, opts)
notification.poller:start_polling(notification.options.poll_rate)
end

Expand All @@ -214,16 +224,16 @@ end
---@param group_key Key|nil Which group to clear
function notification.clear(group_key)
if group_key == nil then
groups = {}
state.groups = {}
else
for idx, group in ipairs(groups) do
for idx, group in ipairs(state.groups) do
if group.key == group_key then
table.remove(groups, idx)
table.remove(state.groups, idx)
break
end
end
end
if #groups == 0 then
if #state.groups == 0 then
notification.window.guard(notification.window.close)
end
end
Expand All @@ -239,13 +249,13 @@ end
notification.poller = poll.Poller {
name = "notification",
poll = function(self)
groups = notification.model.tick(self:now(), groups)
notification.model.tick(self:now(), state)

-- TODO: if not modified, don't re-render
local v = notification.view.render(self:now(), groups)
local v = notification.view.render(self:now(), state.groups)

if #v.lines > 0 then
if view_suppressed then
if state.view_suppressed then
return true
end

Expand All @@ -255,7 +265,7 @@ notification.poller = poll.Poller {
end)
return true
else
if view_suppressed then
if state.view_suppressed then
return false
end

Expand Down Expand Up @@ -289,12 +299,12 @@ end
---@param suppress boolean|nil Whether to suppress or toggle suppression
function notification.suppress(suppress)
if suppress == nil then
view_suppressed = not view_suppressed
state.view_suppressed = not state.view_suppressed
else
view_suppressed = suppress
state.view_suppressed = suppress
end

if view_suppressed then
if state.view_suppressed then
notification.close()
end
end
Expand All @@ -305,7 +315,7 @@ end
---@param item_key Key
---@return boolean successfully_removed
function notification.remove(group_key, item_key)
return notification.model.remove(groups, group_key, item_key)
return notification.model.remove(state, group_key, item_key)
end

return notification
65 changes: 35 additions & 30 deletions lua/fidget/notification/model.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,33 @@
--- (2) to accumulate repeated, asynchronous in-place-updating notifications,
--- and avoid building strings for no reason; and
--- (3) to enable fine-grained cacheing of rendered elements.
---
--- Types and functions defined in this module are considered private, and won't
--- be added to code documentation.
local M = {}

--- A collection of NotificationItems.
--- The abstract state of the notifications subsystem.
---@class State
---@field groups Group[] active notification groups
---@field view_suppressed boolean whether the notification window is suppressed.

--- A collection of notification Items.
---@class Group
---@field key Key used to distinguish this group from others
---@field config Config configuration for this group
---@field items Item[] items displayed in the group

--- Notification element containing a message and optional annotation.
---@class Item
---@field key Key used to distinguish this item from others
---@field message string displayed message for the item
---@field annote string|nil optional title that accompanies the message
---@field style string style used to render the annote/title, if any
---@field hidden boolean whether this item should be shown
---@field expires_at number what time this item should be removed; math.huge means never
---@field data any|nil arbitrary data attached to notification item

--- Get the notification group indexed by group_key; create one if none exists.
---
---@param configs { [Key]: Config }
---@param configs table<Key, Config>
---@param groups Group[]
---@param group_key Key
---@return Group group
---@return Group group
---@return number|nil new_index
local function get_group(configs, groups, group_key)
for _, group in ipairs(groups) do
if group.key == group_key then
return group
return group, nil
end
end

Expand All @@ -48,7 +47,7 @@ local function get_group(configs, groups, group_key)
config = configs[group_key] or configs.default
}
table.insert(groups, group)
return group
return group, #groups
end

--- Search for an item with the given key among a notification group.
Expand Down Expand Up @@ -133,19 +132,22 @@ end
---@protected
---@param now number
---@param configs table<string, Config>
---@param groups Group[]
---@param state State
---@param msg string|nil
---@param level Level|nil
---@param opts Options|nil
function M.update(now, configs, groups, msg, level, opts)
function M.update(now, configs, state, msg, level, opts)
opts = opts or {}
local group_key = opts.group ~= nil and opts.group or "default"
local group = get_group(configs, groups, group_key)
local group, new_index = get_group(configs, state.groups, group_key)
local item = find_item(group, opts.key)

if item == nil then
-- Item doesn't yet exist; create new item and to insert into the group
if msg == nil or opts.update_only then
if new_index then
table.remove(state.groups, new_index)
end
return
end
---@type Item
Expand All @@ -156,6 +158,7 @@ function M.update(now, configs, groups, msg, level, opts)
style = style_from_level(group.config, level) or group.config.annote_style or "Question",
hidden = opts.hidden or false,
expires_at = compute_expiry(now, opts.ttl, group.config.ttl),
last_updated = now,
data = opts.data,
}
table.insert(group.items, new_item)
Expand All @@ -166,26 +169,32 @@ function M.update(now, configs, groups, msg, level, opts)
item.annote = opts.annote or annote_from_level(group.config, level) or item.annote
item.hidden = opts.hidden or item.hidden
item.expires_at = opts.ttl and compute_expiry(now, opts.ttl, group.config.ttl) or item.expires_at
item.last_updated = now
item.data = opts.data ~= nil and opts.data or item.data
end

if new_index then
-- NOTE: we use vim.fn.sort() here because it is stable, and does so in-place.
vim.fn.sort(state.groups, function(a, b) return (a.config.priority or 50) - (b.config.priority or 50) end)
end
end

--- Remove an item from a particular group.
---
---@param groups Group[]
---@param state State
---@param group_key Key
---@param item_key Key
---@return boolean successfully_removed
function M.remove(groups, group_key, item_key)
for g, group in ipairs(groups) do
function M.remove(state, group_key, item_key)
for g, group in ipairs(state.groups) do
if group.key == group_key then
for i, item in ipairs(group.items) do
if item.key == item_key then
-- Note that it should be safe to perform destructive updates to the
-- arrays here since we're no longer iterating.
table.remove(group.items, i)
if #group.items == 0 then
table.remove(groups, g)
table.remove(state.groups, g)
end
return true
end
Expand All @@ -198,16 +207,12 @@ end

--- Prune out all items (and groups) for which the ttl has elapsed.
---
--- Updates each group in-place (i.e., removes items from them), but returns
--- a list of groups that still have items left.
---
---@protected
---@param now number timestamp of current frame.
---@param groups Group[]
---@return Group[]
function M.tick(now, groups)
---@param state State
function M.tick(now, state)
local new_groups = {}
for _, group in ipairs(groups) do
for _, group in ipairs(state.groups) do
local new_items = {}
for _, item in ipairs(group.items) do
if item.expires_at > now then
Expand All @@ -221,7 +226,7 @@ function M.tick(now, groups)
else
end
end
return new_groups
state.groups = new_groups
end

return M

0 comments on commit 1ad95a2

Please sign in to comment.