diff --git a/lua/org-roam/api/node.lua b/lua/org-roam/api/node.lua index 56688d8..f3de0d0 100644 --- a/lua/org-roam/api/node.lua +++ b/lua/org-roam/api/node.lua @@ -270,17 +270,19 @@ local function make_on_post_refile(roam, cb) end end +---Returns a promise when the capture is completed. +---If canceled or invalid, promise yields nil. ---@param roam OrgRoam ---@param opts {origin:string|false|nil, title:string|nil} ----@param cb fun(id:org-roam.core.database.Id|nil) -local function roam_capture_immediate(roam, opts, cb) +---@return OrgPromise +local function roam_capture_immediate(roam, opts) local template = build_template(roam, { target = roam.config.immediate.target, template = roam.config.immediate.template, }, opts) ---@param content string[]|nil - template:compile():next(function(content) + return template:compile():next(function(content) if not content then return notify.echo_info("canceled") end @@ -291,44 +293,42 @@ local function roam_capture_immediate(roam, opts, cb) local expander = make_target_expander(roam, nil, opts) local path = expander(template.target) - io.write_file(path, content_str, function(err) - if err then - notify.error(err) - log.error(err) - return - end - - vim.schedule(function() - roam.database:load_file({ path = path }):next(function(result) - local file = result.file + return io.write_file(path, content_str):next(function() + return path + end) + end):next(function(path) --[[ @cast path string|nil ]] + if not path then return nil end + return roam.database:load_file({ path = path }) + end):next(function(result) --[[ @cast result {file:OrgFile}|nil ]] + if not result then return nil end - -- Look for the id of the newly-captured file - local id = file:get_property("id") + local file = result.file - -- If we don't find a file-level node, look for headline nodes - if not id then - for _, headline in ipairs(file:get_headlines()) do - id = headline:get_property("id", false) - if id then break end - end - end + -- Look for the id of the newly-captured file + local id = file:get_property("id") - -- Trigger the callback regardless of whether we got an id - vim.schedule(function() cb(id) end) + -- If we don't find a file-level node, look for headline nodes + if not id then + for _, headline in ipairs(file:get_headlines()) do + id = headline:get_property("id", false) + if id then break end + end + end - return file - end) - end) - end) + return id + end):catch(function(err) + notify.error(err) + log.error(err) end) end +---Returns a promise when the capture is completed. +---Note that on cancelation of refile, the promise is not resolved. ---@param roam OrgRoam ---@param opts? {immediate?:boolean, origin?:string|false, title?:string, templates?:table} ----@param cb? fun(id:org-roam.core.database.Id|nil) -local function roam_capture(roam, opts, cb) +---@return OrgPromise +local function roam_capture(roam, opts) opts = opts or {} - cb = cb or function() end -- If not provided an origin and want to include the origin, -- use the node under the cursor; skip this if origin is false @@ -337,48 +337,46 @@ local function roam_capture(roam, opts, cb) end if opts.immediate then - roam_capture_immediate(roam, opts, cb) + return roam_capture_immediate(roam, opts) else - local templates = build_templates(roam, { - origin = opts.origin, - title = opts.title, - templates = opts.templates, - }) - local on_pre_refile = make_on_pre_refile(roam, opts) - local on_post_refile = make_on_post_refile(roam, cb) - roam.database:files():next(function(files) - local Capture = require("orgmode.capture") - local capture = Capture:new({ - files = files, - templates = templates, - on_pre_refile = on_pre_refile, - on_post_refile = on_post_refile, + -- TODO: Currently, there is no way in the capture api to support + -- detecting when a refile is canceled. This means that we + -- have no way of fully resolving the promise. To support + -- this properly, we would need to update nvim-orgmode's + -- capture to accept an additional callback on cancelation. + return Promise.new(function(resolve) + local templates = build_templates(roam, { + origin = opts.origin, + title = opts.title, + templates = opts.templates, }) - - return capture:prompt() + local on_pre_refile = make_on_pre_refile(roam, opts) + local on_post_refile = make_on_post_refile(roam, resolve) + roam.database:files():next(function(files) + local Capture = require("orgmode.capture") + local capture = Capture:new({ + files = files, + templates = templates, + on_pre_refile = on_pre_refile, + on_post_refile = on_post_refile, + }) + + return capture:prompt() + end) end) end end ---@param roam OrgRoam ---@param opts? {immediate?:boolean, origin?:string, title?:string, ranges?:org-roam.utils.Range[], templates?:table} ----@param cb? fun(id:org-roam.core.database.Id|nil) -local function roam_insert(roam, opts, cb) +---@return OrgPromise +local function roam_insert(roam, opts) opts = opts or {} local winnr = vim.api.nvim_get_current_win() local bufnr = vim.api.nvim_get_current_buf() local changedtick = vim.api.nvim_buf_get_changedtick(bufnr) local cursor = vim.api.nvim_win_get_cursor(winnr) - ---@param id org-roam.core.database.Id|nil - local function do_cb(id) - if type(cb) == "function" then - vim.schedule(function() - cb(id) - end) - end - end - ---@param id org-roam.core.database.Id ---@param label? string local function insert_link(id, label) @@ -435,52 +433,46 @@ local function roam_insert(roam, opts, cb) vim.cmd.stopinsert() end - roam.ui.select_node({ - allow_select_missing = true, - auto_select = opts.immediate, - init_input = opts.title, - }) - :on_choice(function(choice) - insert_link(choice.id, choice.label) - do_cb(choice.id) - end) - :on_choice_missing(function(label) - if roam.config.capture.include_origin and not opts.origin then - opts.origin = node_id_under_cursor_sync({ win = winnr }) - end - - roam_capture(roam, { - immediate = opts.immediate, - origin = opts.origin, - title = label, - templates = opts.templates, - }, function(id) - do_cb(id) - if id then - insert_link(id) - return + return Promise.new(function(resolve) + roam.ui.select_node({ + allow_select_missing = true, + auto_select = opts.immediate, + init_input = opts.title, + }) + :on_choice(function(choice) + insert_link(choice.id, choice.label) + resolve(choice.id) + end) + :on_choice_missing(function(label) + if roam.config.capture.include_origin and not opts.origin then + opts.origin = node_id_under_cursor_sync({ win = winnr }) end + + roam_capture(roam, { + immediate = opts.immediate, + origin = opts.origin, + title = label, + templates = opts.templates, + }):next(function(id) + if id then + insert_link(id) + end + + resolve(id) + return id + end) end) - end) - :open() + :open() + end) end ---@param roam OrgRoam ---@param opts? {origin?:string, title?:string, templates?:table} ----@param cb? fun(id:org-roam.core.database.Id|nil) -local function roam_find(roam, opts, cb) +---@return OrgPromise +local function roam_find(roam, opts) opts = opts or {} local winnr = vim.api.nvim_get_current_win() - ---@param id org-roam.core.database.Id|nil - local function do_cb(id) - if type(cb) == "function" then - vim.schedule(function() - cb(id) - end) - end - end - ---@param id org-roam.core.database.Id local function visit_node(id) local node = roam.database:get_sync(id) @@ -499,32 +491,35 @@ local function roam_find(roam, opts, cb) vim.cmd.stopinsert() end - roam.ui.select_node({ - allow_select_missing = true, - init_input = opts.title, - }) - :on_choice(function(choice) - visit_node(choice.id) - do_cb(choice.id) - end) - :on_choice_missing(function(label) - if roam.config.capture.include_origin and not opts.origin then - opts.origin = node_id_under_cursor_sync({ win = winnr }) - end - - roam_capture(roam, { - origin = opts.origin, - title = label, - templates = opts.templates, - }, function(id) - do_cb(id) - if id then - visit_node(id) - return + return Promise.new(function(resolve) + roam.ui.select_node({ + allow_select_missing = true, + init_input = opts.title, + }) + :on_choice(function(choice) + visit_node(choice.id) + resolve(choice.id) + end) + :on_choice_missing(function(label) + if roam.config.capture.include_origin and not opts.origin then + opts.origin = node_id_under_cursor_sync({ win = winnr }) end + + roam_capture(roam, { + origin = opts.origin, + title = label, + templates = opts.templates, + }):next(function(id) + if id then + visit_node(id) + end + + resolve(id) + return id + end) end) - end) - :open() + :open() + end) end ---@param roam OrgRoam @@ -536,9 +531,9 @@ return function(roam) ---Creates a node if it does not exist, and restores the current window ---configuration upon completion. ---@param opts? {immediate?:boolean, origin?:string|false, title?:string, templates?:table} - ---@param cb? fun(id:org-roam.core.database.Id|nil) - function M.capture(opts, cb) - return roam_capture(roam, opts, cb) + ---@return OrgPromise + function M.capture(opts) + return roam_capture(roam, opts) end ---Creates a node if it does not exist, and inserts a link to the node @@ -552,16 +547,16 @@ return function(roam) ---versus inserting at point. ---where everything uses 1-based indexing and inclusive. ---@param opts? {immediate?:boolean, origin?:string, title?:string, ranges?:org-roam.utils.Range[], templates?:table} - ---@param cb? fun(id:org-roam.core.database.Id|nil) - function M.insert(opts, cb) - return roam_insert(roam, opts, cb) + ---@return OrgPromise + function M.insert(opts) + return roam_insert(roam, opts) end ---Creates a node if it does not exist, and visits the node. ---@param opts? {origin?:string, title?:string, templates?:table} - ---@param cb? fun(id:org-roam.core.database.Id|nil) - function M.find(opts, cb) - return roam_find(roam, opts, cb) + ---@return OrgPromise + function M.find(opts) + return roam_find(roam, opts) end return M diff --git a/lua/org-roam/core/database.lua b/lua/org-roam/core/database.lua index 4f32c7b..e304f00 100644 --- a/lua/org-roam/core/database.lua +++ b/lua/org-roam/core/database.lua @@ -11,6 +11,8 @@ local Queue = require("org-roam.core.utils.queue") local random = require("org-roam.core.utils.random") local tbl_utils = require("org-roam.core.utils.table") +local Promise = require("orgmode.utils.promise") + local DEFAULT_MAX_NODES = 2 ^ 31 local DEFAULT_MAX_DISTANCE = 2 ^ 31 @@ -101,30 +103,25 @@ end function M:load_from_disk_sync(path, opts) opts = opts or {} - local f = async.wrap( - M.load_from_disk, - { - time = opts.time, - interval = opts.interval, - n = 2, - } - ) + ---@type boolean, string|org-roam.core.Database + local ok, data = pcall(function() + return M:load_from_disk(path) + end) - return f(self, path) + if ok then + ---@cast data -string + return nil, data + else + ---@cast data string + return data, nil + end end ---Asynchronously loads database from disk. ---@param path string where to find the database ----@param cb fun(err:string|nil, db:org-roam.core.Database|nil) -function M:load_from_disk(path, cb) - io.read_file(path, function(err, data) - if err then - cb(err) - return - end - - assert(data, "impossible: data nil") - +---@return OrgPromise +function M:load_from_disk(path) + return io.read_file(path):next(function(data) -- Try to decode the data into Lua and set it as the nodes, -- using each potential decoder in turn ---@type boolean, table|nil @@ -145,8 +142,7 @@ function M:load_from_disk(path, cb) if __data then errmsg = errmsg .. ": " .. vim.inspect(__data) end - vim.schedule(function() cb(errmsg) end) - return + return Promise.reject(errmsg) end local db = M:new() @@ -160,42 +156,29 @@ function M:load_from_disk(path, cb) ---@diagnostic disable-next-line:invisible db.__indexes = __data.indexes - vim.schedule(function() cb(nil, db) end) + return db end) end ---Synchronously writes database to disk. ---- ----Note: cannot be called within fast callbacks. ---- ----Accepts options to configure how to wait. ---- ----* `time`: the milliseconds to wait for writing to finish. ---- Defaults to waiting forever. ----* `interval`: the millseconds between attempts to check that writing ---- has finished. Defaults to 200 milliseconds. ---@param path string where to store the database ----@param opts? {time?:integer,interval?:integer} +---@param opts? {timeout?:integer} ---@return string|nil err function M:write_to_disk_sync(path, opts) opts = opts or {} - local f = async.wrap( - M.write_to_disk, - { - time = opts.time, - interval = opts.interval, - n = 2, - } - ) + ---@type boolean, string|nil + local _, err = pcall(function() + M:write_to_disk(path):wait(opts.timeout) + end) - return f(self, path) + return err end ---Asynchronously writes database to disk. ---@param path string where to store the database ----@param cb fun(err:string|nil) -function M:write_to_disk(path, cb) +---@return OrgPromise +function M:write_to_disk(path) ---@type fun(obj:any):string local encode if self.__cache_type == "json" then @@ -204,7 +187,9 @@ function M:write_to_disk(path, cb) encode = vim.mpack.encode end - assert(encode, "invalid cache type: " .. self.__cache_type) + if not encode then + return Promise.reject("invalid cache type: " .. self.__cache_type) + end local db_contents = { nodes = self.__nodes, @@ -221,11 +206,10 @@ function M:write_to_disk(path, cb) if data then errmsg = errmsg .. ": " .. vim.inspect(data) end - cb(errmsg) - return + return Promise.reject(errmsg) end - io.write_file(path, data, cb) + return io.write_file(path, data) end ---Inserts non-false data into the database as a node with no edges. diff --git a/lua/org-roam/core/utils/io.lua b/lua/org-roam/core/utils/io.lua index 6415bc6..4f86a8d 100644 --- a/lua/org-roam/core/utils/io.lua +++ b/lua/org-roam/core/utils/io.lua @@ -29,7 +29,13 @@ local M = {} ---@return string|nil err function M.write_file_sync(path, data, opts) opts = opts or {} - return M.write_file(path, data):wait(opts.timeout) + + ---@type boolean, string|nil + local _, err = pcall(function() + M.write_file(path, data):wait(opts.timeout) + end) + + return err end ---Write some data asynchronously to disk, creating the file or overwriting @@ -85,7 +91,13 @@ end ---@return string|nil err, string|nil data function M.read_file_sync(path, opts) opts = opts or {} - return M.read_file(path):wait(opts.timeout) + + ---@type boolean, string + local ok, data = pcall(function() + return M.read_file(path):wait(opts.timeout) + end) + + return not ok and data or nil, ok and data or nil end ---Read some data asynchronously from disk. @@ -131,7 +143,19 @@ end ---@return string|nil err, uv.aliases.fs_stat_table|nil stat function M.stat_sync(path, opts) opts = opts or {} - return M.stat(path):wait(opts.timeout) + + ---@type boolean, string|uv.aliases.fs_stat_table + local ok, data = pcall(function() + return M.stat(path):wait(opts.timeout) + end) + + if ok then + ---@cast data -string + return nil, data + else + ---@cast data string + return data, nil + end end ---Obtains information about the file pointed to by `path`. Read, write, or @@ -157,7 +181,19 @@ end ---@return string|nil err, boolean|nil success function M.unlink_sync(path, opts) opts = opts or {} - return M.unlink(path):wait(opts.timeout) + + ---@type boolean, string|boolean + local ok, data = pcall(function() + return M.unlink(path):wait(opts.timeout) + end) + + if ok then + ---@cast data -string + return nil, data + else + ---@cast data string + return data, nil + end end ---Removes the file specified by `path`. diff --git a/lua/org-roam/database.lua b/lua/org-roam/database.lua index 9937023..ea76661 100644 --- a/lua/org-roam/database.lua +++ b/lua/org-roam/database.lua @@ -184,26 +184,13 @@ function M:save(opts) -- Refresh our data (no rescan or force) to make sure it is fresh return self:load():next(function() - return Promise.new(function(resolve, reject) - db:write_to_disk(self.__database_path, function(err) - if err then - -- NOTE: Scheduling to avoid potential textlock issue - vim.schedule(function() - reject(err) - end) - return - end - - -- NOTE: Scheduling to avoid potential textlock issue - vim.schedule(function() - profiler:stop(rec_id) - log.fmt_debug("saving database took %s", - profiler:time_taken_as_string({ recording = rec_id })) - - self.__last_save = db:changed_tick() - resolve(true) - end) - end) + return db:write_to_disk(self.__database_path):next(function() + profiler:stop(rec_id) + log.fmt_debug("saving database took %s", + profiler:time_taken_as_string({ recording = rec_id })) + + self.__last_save = db:changed_tick() + return true end) end) end) @@ -213,24 +200,15 @@ end ---@return OrgPromise function M:delete_disk_cache() return Promise.new(function(resolve, reject) - io.stat(self.__database_path, function(err, stat) - if err or not stat then - return vim.schedule(function() - resolve(false) - end) - end - - io.unlink(self.__database_path, function(err, success) - if err then - return vim.schedule(function() - reject(err) - end) - end - - return vim.schedule(function() - resolve(success or false) - end) + io.stat(self.__database_path):next(function() + return io.unlink(self.__database_path):next(function(success) + resolve(success) + return success + end):catch(function(err) + reject(err) end) + end):catch(function() + resolve(false) end) end) end diff --git a/lua/org-roam/database/loader.lua b/lua/org-roam/database/loader.lua index 3671064..f64e038 100644 --- a/lua/org-roam/database/loader.lua +++ b/lua/org-roam/database/loader.lua @@ -356,44 +356,29 @@ end function M:database() return self.__db and Promise.resolve(self.__db) or Promise.new(function(resolve) -- Load our database from disk if it is available - io.stat(self.path.database, function(unavailable) - if unavailable then - local db = Database:new() + io.stat(self.path.database):next(function() + return Database:load_from_disk(self.path.database):next(function(db) schema:update(db) self.__db = db - -- NOTE: Scheduling to avoid textlock issues in promise - -- resolution - return vim.schedule(function() - resolve(db) - end) - end - - Database:load_from_disk(self.path.database, function(err, db) - if err then - -- NOTE: Scheduling to avoid textlock issues - return vim.schedule(function() - log.fmt_error("Failed to load database: %s", err) - - -- Set up database with a clean slate instead - db = Database:new() - schema:update(db) - self.__db = db + resolve(db) + return db + end):catch(function(err) + log.fmt_error("Failed to load database: %s", err) - resolve(db) - end) - end - - ---@cast db -nil + -- Set up database with a clean slate instead + local db = Database:new() schema:update(db) self.__db = db - -- NOTE: Scheduling to avoid textlock issues in promise - -- resolution - return vim.schedule(function() - resolve(db) - end) + resolve(db) end) + end):catch(function() + local db = Database:new() + schema:update(db) + self.__db = db + + return resolve(db) end) end) end diff --git a/lua/org-roam/extensions/dailies.lua b/lua/org-roam/extensions/dailies.lua index c4ae9b2..04f3fba 100644 --- a/lua/org-roam/extensions/dailies.lua +++ b/lua/org-roam/extensions/dailies.lua @@ -249,26 +249,23 @@ return function(roam) if date then local path = date_to_path(roam, date) return Promise.new(function(resolve) - io.stat(path, function(err, stat) - vim.schedule(function() - if err or not stat then - local buf = make_daily_buffer(roam, date) - pcall(vim.api.nvim_win_set_buf, win, buf) - - -- NOTE: Must perform detection when buffer - -- is first created in order for folding - -- and other functionality to work! - vim.api.nvim_buf_call(buf, function() - vim.cmd("filetype detect") - end) - - return resolve(date) - else - pcall(vim.api.nvim_set_current_win, win) - vim.cmd.edit(path) - return resolve(date) - end + io.stat(path):next(function(stat) + pcall(vim.api.nvim_set_current_win, win) + vim.cmd.edit(path) + resolve(date) + return stat + end):catch(function() + local buf = make_daily_buffer(roam, date) + pcall(vim.api.nvim_win_set_buf, win, buf) + + -- NOTE: Must perform detection when buffer + -- is first created in order for folding + -- and other functionality to work! + vim.api.nvim_buf_call(buf, function() + vim.cmd("filetype detect") end) + + return resolve(date) end) end) else diff --git a/spec/core_utils_io_spec.lua b/spec/core_utils_io_spec.lua index f056fc0..f653b2c 100644 --- a/spec/core_utils_io_spec.lua +++ b/spec/core_utils_io_spec.lua @@ -73,9 +73,12 @@ describe("org-roam.core.utils.io", function() local is_done = false local path = vim.fn.tempname() - utils_io.read_file(path, function(err, d) - error = err + utils_io.read_file(path):next(function(d) data = d + return d + end):catch(function(err) + error = err + end):finally(function() is_done = true end) @@ -90,9 +93,12 @@ describe("org-roam.core.utils.io", function() local is_done = false local path = create_temp_file("hello world") - utils_io.read_file(path, function(err, d) - error = err + utils_io.read_file(path):next(function(d) data = d + return d + end):catch(function(err) + error = err + end):finally(function() is_done = true end) @@ -131,8 +137,9 @@ describe("org-roam.core.utils.io", function() local is_done = false local path = vim.fn.tempname() - utils_io.write_file(path, "hello world", function(err) + utils_io.write_file(path, "hello world"):catch(function(err) error = err + end):finally(function() is_done = true end) @@ -150,8 +157,9 @@ describe("org-roam.core.utils.io", function() local path = create_temp_file("test") - utils_io.write_file(path, "hello world", function(err) + utils_io.write_file(path, "hello world"):catch(function(err) error = err + end):finally(function() is_done = true end) @@ -191,9 +199,12 @@ describe("org-roam.core.utils.io", function() local is_done = false local path = vim.fn.tempname() - utils_io.stat(path, function(err, s) - error = err + utils_io.stat(path):next(function(s) stat = s + return s + end):catch(function(err) + error = err + end):finally(function() is_done = true end) @@ -208,9 +219,12 @@ describe("org-roam.core.utils.io", function() local is_done = false local path = create_temp_file("test") - utils_io.stat(path, function(err, s) - error = err + utils_io.stat(path):next(function(s) stat = s + return s + end):catch(function(err) + error = err + end):finally(function() is_done = true end)