From 73753403def5938d71464a980c57e513e75d35fa Mon Sep 17 00:00:00 2001 From: Kristijan Husak Date: Tue, 8 Jul 2025 14:57:05 +0200 Subject: [PATCH] feat(async)!: Switch to vim.async --- lua/orgmode/agenda/init.lua | 18 ++--- lua/orgmode/init.lua | 11 +-- lua/orgmode/objects/calendar.lua | 12 ++-- lua/orgmode/org/global.lua | 5 +- lua/orgmode/org/mappings.lua | 93 ++++++++++++------------- lua/orgmode/utils/async.lua | 112 +++++++++++++++++++++++++++++++ 6 files changed, 187 insertions(+), 64 deletions(-) create mode 100644 lua/orgmode/utils/async.lua diff --git a/lua/orgmode/agenda/init.lua b/lua/orgmode/agenda/init.lua index ac8571f06..d35984583 100644 --- a/lua/orgmode/agenda/init.lua +++ b/lua/orgmode/agenda/init.lua @@ -335,15 +335,15 @@ function Agenda:goto_date() return utils.echo_error('No available views to jump to date.') end - return Calendar.new({ date = Date.now(), title = 'Go to agenda date' }):open():next(function(date) - if not date then - return nil - end - for _, view in ipairs(views) do - view:goto_date(date) - end - return self:render() - end) + local date = Calendar.new({ date = Date.now(), title = 'Go to agenda date' }):open() + + if not date then + return nil + end + for _, view in ipairs(views) do + view:goto_date(date) + end + return self:render() end function Agenda:switch_to_item() diff --git a/lua/orgmode/init.lua b/lua/orgmode/init.lua index 444c884bc..9875efac8 100644 --- a/lua/orgmode/init.lua +++ b/lua/orgmode/init.lua @@ -170,9 +170,12 @@ function Org.action(cmd, opts) item = item[part] end end - if item and item[parts[#parts]] then - local method = item[parts[#parts]] - local success, result = pcall(method, item, opts) + if not item or not item[parts[#parts]] then + return + end + + return _G.Org.async.run(function() + local success, result = pcall(item[parts[#parts]], item, opts) if not success then if result.message then return require('orgmode.utils').echo_error(result.message) @@ -183,7 +186,7 @@ function Org.action(cmd, opts) end Org._set_dot_repeat(cmd, opts) return result - end + end) end function Org.cron(opts) diff --git a/lua/orgmode/objects/calendar.lua b/lua/orgmode/objects/calendar.lua index cfe392b68..67ccd92b5 100644 --- a/lua/orgmode/objects/calendar.lua +++ b/lua/orgmode/objects/calendar.lua @@ -9,6 +9,7 @@ local Input = require('orgmode.ui.input') ---@alias OrgCalendarOnRenderDayOpts { line: number, from: number, to: number, buf: number, namespace: number } ---@alias OrgCalendarOnRenderDay fun(day: OrgDate, opts: OrgCalendarOnRenderDayOpts) +---@alias OrgCalendarCallback fun(date: OrgDate | nil, cleared?: boolean) local SelState = { DAY = 0, HOUR = 1, MIN_BIG = 2, MIN_SMALL = 3 } local big_minute_step = config.calendar.min_big_step @@ -17,7 +18,7 @@ local small_minute_step = config.calendar.min_small_step or config.org_time_stam ---@class OrgCalendar ---@field win number? ---@field buf number? ----@field callback fun(date: OrgDate | nil, cleared?: boolean) +---@field callback OrgCalendarCallback ---@field date OrgDate? ---@field title? string ---@field on_day? OrgCalendarOnRenderDay @@ -57,7 +58,8 @@ local height = 14 local x_offset = 1 -- one border cell local y_offset = 2 -- one border cell and one padding cell ----@return OrgPromise +---@async +---@return OrgDate|nil,boolean|nil function Calendar:open() local get_window_opts = function() return { @@ -171,8 +173,10 @@ function Calendar:open() self:set_day() end, map_opts) self:jump_day() - return Promise.new(function(resolve) - self.callback = resolve + ---@param cb OrgCalendarCallback + ---@diagnostic disable-next-line: return-type-mismatch + return Org.async.await(1, function(cb) + self.callback = cb end) end diff --git a/lua/orgmode/org/global.lua b/lua/orgmode/org/global.lua index f72330529..90d30169b 100644 --- a/lua/orgmode/org/global.lua +++ b/lua/orgmode/org/global.lua @@ -51,6 +51,7 @@ local build = function(orgmode) local config = require('orgmode.config') local OrgGlobal = { + async = require('orgmode.utils.async'), help = function() vim.cmd(('tabnew %s'):format(('%s/%s'):format(docs_dir, 'index.org'))) vim.cmd(('tcd %s'):format(docs_dir)) @@ -134,7 +135,9 @@ end, { if type(item) ~= 'table' then return {} end - local list = vim.tbl_keys(item) + local list = vim.tbl_filter(function(key) + return key ~= 'async' + end, vim.tbl_keys(item)) if arg_lead == '' then return list diff --git a/lua/orgmode/org/mappings.lua b/lua/orgmode/org/mappings.lua index fb23ecf4e..74b214380 100644 --- a/lua/orgmode/org/mappings.lua +++ b/lua/orgmode/org/mappings.lua @@ -310,11 +310,10 @@ function OrgMappings:change_date() if not date then return end - return Calendar.new({ date = date, title = 'Change date' }):open():next(function(new_date) - if new_date then - self:_replace_date(new_date) - end - end) + local new_date = Calendar.new({ date = date, title = 'Change date' }):open() + if new_date then + self:_replace_date(new_date) + end end function OrgMappings:priority_up() @@ -975,35 +974,39 @@ end function OrgMappings:org_deadline() local headline = self.files:get_closest_headline() local deadline_date = headline:get_deadline_date() - return Calendar.new({ date = deadline_date or Date.today(), clearable = true, title = 'Set deadline' }) - :open() - :next(function(new_date, cleared) - if cleared then - return headline:remove_deadline_date() - end - if not new_date then - return nil - end - headline:remove_closed_date() - headline:set_deadline_date(new_date) - end) + local new_date, cleared = Calendar.new({ + date = deadline_date or Date.today(), + clearable = true, + title = 'Set deadline', + }):open() + + if cleared then + return headline:remove_deadline_date() + end + if not new_date then + return nil + end + headline:remove_closed_date() + headline:set_deadline_date(new_date) end function OrgMappings:org_schedule() local headline = self.files:get_closest_headline() local scheduled_date = headline:get_scheduled_date() - return Calendar.new({ date = scheduled_date or Date.today(), clearable = true, title = 'Set schedule' }) - :open() - :next(function(new_date, cleared) - if cleared then - return headline:remove_scheduled_date() - end - if not new_date then - return nil - end - headline:remove_closed_date() - headline:set_scheduled_date(new_date) - end) + local new_date, cleared = Calendar.new({ + date = scheduled_date or Date.today(), + clearable = true, + title = 'Set schedule', + }):open() + + if cleared then + return headline:remove_scheduled_date() + end + if not new_date then + return nil + end + headline:remove_closed_date() + headline:set_scheduled_date(new_date) end ---@param inactive boolean @@ -1011,27 +1014,25 @@ function OrgMappings:org_time_stamp(inactive) local date = self:_get_date_under_cursor() if date then - return Calendar.new({ date = date, title = 'Replace date' }):open():next(function(new_date) - if not new_date then - return - end - self:_replace_date(new_date) - end) + local new_date = Calendar.new({ date = date, title = 'Replace date' }):open() + if not new_date then + return + end + return self:_replace_date(new_date) end local date_start = self:_get_date_under_cursor(-1) - return Calendar.new({ date = Date.today() }):open():next(function(new_date) - if not new_date then - return nil - end - local date_string = new_date:to_wrapped_string(not inactive) - if date_start then - date_string = '--' .. date_string - vim.cmd('norm!x') - end - vim.cmd(string.format('norm!a%s', date_string)) - end) + local new_date = Calendar.new({ date = Date.today() }):open() + if not new_date then + return nil + end + local date_string = new_date:to_wrapped_string(not inactive) + if date_start then + date_string = '--' .. date_string + vim.cmd('norm!x') + end + vim.cmd(string.format('norm!a%s', date_string)) end function OrgMappings:org_toggle_timestamp_type() diff --git a/lua/orgmode/utils/async.lua b/lua/orgmode/utils/async.lua new file mode 100644 index 000000000..fa1e37de8 --- /dev/null +++ b/lua/orgmode/utils/async.lua @@ -0,0 +1,112 @@ +---@taken from Neovim master branch vim._async + +local M = {} + +local max_timeout = 30000 +local copcall = package.loaded.jit and pcall or require('coxpcall').pcall + +--- @param thread thread +--- @param on_finish fun(err: string?, ...:any) +--- @param ... any +local function resume(thread, on_finish, ...) + --- @type {n: integer, [1]:boolean, [2]:string|function} + local ret = vim.F.pack_len(coroutine.resume(thread, ...)) + local stat = ret[1] + + if not stat then + -- Coroutine had error + on_finish(ret[2] --[[@as string]]) + elseif coroutine.status(thread) == 'dead' then + -- Coroutine finished + on_finish(nil, unpack(ret, 2, ret.n)) + else + local fn = ret[2] + --- @cast fn -string + + --- @type boolean, string? + local ok, err = copcall(fn, function(...) + resume(thread, on_finish, ...) + end) + + if not ok then + on_finish(err) + end + end +end + +--- @param func async fun(): ...:any +--- @param on_finish? fun(err: string?, ...:any) +function M.run(func, on_finish) + local res --- @type {n:integer, [integer]:any}? + resume(coroutine.create(func), function(err, ...) + res = vim.F.pack_len(err, ...) + if on_finish then + on_finish(err, ...) + end + end) + + return { + --- @param timeout? integer + --- @return any ... return values of `func` + wait = function(_self, timeout) + vim.wait(timeout or max_timeout, function() + return res ~= nil + end) + assert(res, 'timeout') + if res[1] then + error(res[1]) + end + return unpack(res, 2, res.n) + end, + } +end + +--- Asynchronous blocking wait +--- @async +--- @param argc integer +--- @param fun function +--- @param ... any func arguments +--- @return any ... +function M.await(argc, fun, ...) + assert(coroutine.running(), 'Async.await() must be called from an async function') + local args = vim.F.pack_len(...) --- @type {n:integer, [integer]:any} + + --- @param callback fun(...:any) + return coroutine.yield(function(callback) + args[argc] = assert(callback) + fun(unpack(args, 1, math.max(argc, args.n))) + end) +end + +--- @async +--- @param max_jobs integer +--- @param funs (async fun())[] +function M.join(max_jobs, funs) + if #funs == 0 then + return + end + + max_jobs = math.min(max_jobs, #funs) + + --- @type (async fun())[] + local remaining = { select(max_jobs + 1, unpack(funs)) } + local to_go = #funs + + M.await(1, function(on_finish) + local function run_next() + to_go = to_go - 1 + if to_go == 0 then + on_finish() + elseif #remaining > 0 then + local next_fun = table.remove(remaining) + M.run(next_fun, run_next) + end + end + + for i = 1, max_jobs do + M.run(funs[i], run_next) + end + end) +end + +return M