local M = {} M.debug = false --- @class Signal --- @field name? string --- @field private changing boolean --- @field value any --- @field subscribers table local Signal = {} M.Signal = Signal Signal.__index = Signal --- @param value any --- @param name? string --- @return Signal function Signal:new(value, name) local obj = setmetatable({ name = name, changing = false, value = value, subscribers = {}, }, self) return obj end --- @param value any function Signal:set(value) if vim.deep_equal(self.value, value) then return end self.value = value vim.defer_fn(function() self:_notify() end, 0) end --- @return any function Signal:get() local ctx = M.ExecutionContext.current() if ctx then ctx:track(self) end return self.value end --- @param fn function function Signal:update(fn) self:set(fn(self.value)) end --- @private function Signal:_notify() if self.changing then vim.schedule(function() self:_notify() end) return end local old_changing = self.changing self.changing = true for _, cb in ipairs(self.subscribers) do cb(self.value) end self.changing = old_changing end --- @param callback function function Signal:subscribe(callback) table.insert(self.subscribers, callback) return function() self:unsubscribe(callback) end end --- @param callback function function Signal:unsubscribe(callback) for i, cb in ipairs(self.subscribers) do if cb == callback then table.remove(self.subscribers, i) break end end end function Signal:dispose() self.subscribers = {} end CURRENT_CONTEXT = nil --- @class ExecutionContext --- @field signals table local ExecutionContext = {} M.ExecutionContext = ExecutionContext ExecutionContext.__index = ExecutionContext --- @return ExecutionContext function ExecutionContext:new() return setmetatable({ signals = {}, subscribers = {}, }, ExecutionContext) end function ExecutionContext.current() return CURRENT_CONTEXT end --- @param fn function --- @param ctx ExecutionContext function ExecutionContext:run(fn, ctx) local oldCtx = CURRENT_CONTEXT CURRENT_CONTEXT = ctx local result local success, err = pcall(function() result = fn() end) CURRENT_CONTEXT = oldCtx if not success then error(err) end return result end function ExecutionContext:track(signal) self.signals[signal] = true end --- @param callback function function ExecutionContext:subscribe(callback) local wrapped_callback = function() callback() end for signal in pairs(self.signals) do signal:subscribe(wrapped_callback) end return function() for signal in pairs(self.signals) do signal:unsubscribe(wrapped_callback) end end end function ExecutionContext:dispose() for signal, _ in pairs(self.signals) do signal:dispose() end self.signals = {} end --- @param value any --- @param name? string --- @return Signal function M.create_signal(value, name) return Signal:new(value, name) end --- @param fn function --- @param name? string --- @return Signal function M.create_memo(fn, name) local result M.create_effect(function() local value = fn() if name and M.debug then vim.notify(name) end if result then result:set(value) else result = M.create_signal(value, name and ('m.s:' .. name) or nil) end end, name) return result end --- @param fn function --- @param name? string function M.create_effect(fn, name) local ctx = M.ExecutionContext:new() M.ExecutionContext:run(fn, ctx) return ctx:subscribe(function() if name and M.debug then local deps = vim .iter(vim.tbl_keys(ctx.signals)) :map(function(s) return s.name end) :filter(function(nm) return nm ~= nil end) :join ',' vim.notify(name .. ':=>' .. deps) end fn() end) end return M