Skip to content

Commit

Permalink
feat: add a way to cache state between restarts of Neovim
Browse files Browse the repository at this point in the history
This is useful for features such as `org-clock-in-last` that has the
ability to persist its "knowledge" of the last clocked in task. Org mode
does this by saving to a cache file on system. This commit gives
nvim-orgmode the same capability.
  • Loading branch information
PriceHiller committed Oct 31, 2023
1 parent 47b2978 commit d164905
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 1 deletion.
92 changes: 92 additions & 0 deletions lua/orgmode/state/state.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
local utils = require('orgmode.utils')
local Promise = require('orgmode.utils.promise')

local State = { data = {}, _ctx = { loaded = false, curr_loader = nil } }

local cache_path = vim.fs.normalize(
vim.fn.stdpath('cache') .. '/org-cache.json',
{ expand_env = false })
--- Returns the current State singleton
function State:new()
-- This is done so we can later iterate the 'data'
-- subtable cleanly and shove it into a cache
setmetatable(State, {
__index = function(tbl, key) return tbl.data[key] end,
__newindex = function(tbl, key, value) tbl.data[key] = value end
})
-- Start trying to load the state from cache as part of initializing the state
self:load()
return self
end

---Save the current state to cache
---@return Promise
function State:save()
--- We want to ensure the state was loaded before saving.
return self:load():next(function(_)
utils.writefile(cache_path, vim.json.encode(State.data)):next(
function(_) end, function(err_msg)
vim.schedule_wrap(function()
utils.echo_warning('Failed to save current state! Error: ' .. err_msg)
end)
end)
end, function(_) end)
end

---Load the state cache into the current state
---@return Promise
function State:load()
--- If we currently have a loading operation already running, return that
--- promise. This avoids a race condition of sorts as without this there's
--- potential to have two State:load operations occuring and whichever
--- finishes last sets the state. Not desirable.
if self._ctx.curr_loader ~= nil then return self._ctx.curr_loader end

--- If we've already loaded the state from cache we don't need to do so again
if self._ctx.loaded then
return Promise.new(function(resolve, _) return resolve() end)
end

self._ctx.curr_loader = utils.readfile(cache_path, { raw = true }):next(
function(data)
local success, decoded = pcall(vim.json.decode, data, {
luanil = { object = true, array = true }
})
self._ctx.curr_loader = nil
if not success then
vim.schedule(function()
utils.echo_warning('Failed to save current state! Error: ' .. decoded)
-- Try to 'repair' the cache by saving the current state
self:save()
end)
end
-- Because the state cache repair happens potentially after the data has
-- been added to the cache, we need to ensure the decoded table is set to
-- empty if we got an error back on the json decode operation.
if type(decoded) ~= "table" then
decoded = {}
end

self._ctx.loaded = true
-- It is possible that while the state was loading from cache values
-- were saved into the state. We want to preference the newer values in
-- the state and still get whatever values may not have been set in the
-- interim of the load operation.
self.data = vim.tbl_deep_extend('force', decoded, self.data)
return self
end, ---@param err string
function(err)
-- If the file didn't exist then go ahead and save
-- our current cache and as a side effect create the file
if type(err) == 'string' and err:match([[^ENOENT.*]]) then
self:save()
else
-- If the file did exist, something is wrong. Kick this to the top
error(err)
end
end)

return self._ctx.curr_loader
end

return State:new()
32 changes: 31 additions & 1 deletion lua/orgmode/utils/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ local debounce_timers = {}
local query_cache = {}
local tmp_window_augroup = vim.api.nvim_create_augroup('OrgTmpWindow', { clear = true })

function utils.readfile(file)
function utils.readfile(file, opts)
opts = vim.tbl_deep_extend("keep", opts or {}, { raw = false })
return Promise.new(function(resolve, reject)
uv.fs_open(file, 'r', 438, function(err1, fd)
if err1 then
Expand All @@ -24,6 +25,9 @@ function utils.readfile(file)
if err4 then
return reject(err4)
end
if opts.raw then
return resolve(data)
end
local lines = vim.split(data, '\n')
table.remove(lines, #lines)
return resolve(lines)
Expand All @@ -34,6 +38,32 @@ function utils.readfile(file)
end)
end

function utils.writefile(file, data)
return Promise.new(function(resolve, reject)
uv.fs_open(file, 'w', 438, function(err1, fd)
if err1 then
return reject(err1)
end
uv.fs_fstat(fd, function(err2, stat)
if err2 then
return reject(err2)
end
uv.fs_write(fd, data, nil, function(err3, bytes)
if err3 then
return reject(err3)
end
uv.fs_close(fd, function(err4)
if err4 then
return reject(err4)
end
return resolve(bytes)
end)
end)
end)
end)
end)
end

function utils.open(target)
if vim.fn.executable('xdg-open') == 1 then
return vim.fn.system(string.format('xdg-open %s', target))
Expand Down

0 comments on commit d164905

Please sign in to comment.