From 8a29ed75d5dea6b3b31087b4c65a0f50deb55481 Mon Sep 17 00:00:00 2001 From: robin Date: Fri, 17 Jul 2026 13:28:38 +0200 Subject: [PATCH] feat!: add async support - `utils.make_cmd` now needs async context - `live = false` is now required for non-live pickers that supply `self.get_items` --- lua/artio/builtins.lua | 165 +++++++++++++++++++++-------------------- lua/artio/picker.lua | 22 ++++-- lua/artio/utils.lua | 62 ++++++++++++---- lua/artio/view.lua | 4 +- 4 files changed, 151 insertions(+), 102 deletions(-) diff --git a/lua/artio/builtins.lua b/lua/artio/builtins.lua index 3827ba5..7fe2324 100644 --- a/lua/artio/builtins.lua +++ b/lua/artio/builtins.lua @@ -49,39 +49,38 @@ builtins.files = function(props) props.findprg = props.findprg or findprg local base_dir = props.base_dir or vim.fn.getcwd(0) - local lst = utils.make_cmd(props.findprg, { - cwd = base_dir, - })() - return artio.generic( - lst, - extend({ - prompt = "files", - on_close = function(text, _) - artio.schedule(function() - utils.edit(text) - end) - end, - format_item = function(item) - return vim.fs.relpath(base_dir, item) or item - end, - get_icon = config.get().opts.use_icons and function(item) - return require("mini.icons").get("file", item.v) - end or nil, - preview_item = function(item) - return { buf = vim.fn.bufadd(item) } - end, - actions = extend( - {}, - utils.make_setqflistactions(function(item) - return { filename = item.v } - end), - utils.make_fileactions(function(item) - return vim.fn.bufnr(item.v, true) - end) - ), - }, props) - ) + return artio.pick(extend({ + prompt = "files", + live = false, + get_items = utils.make_cmd(props.findprg, { + cwd = base_dir, + }), + fn = artio.sorter, + on_close = function(text, _) + artio.schedule(function() + utils.edit(text) + end) + end, + format_item = function(item) + return vim.fs.relpath(base_dir, item) or item + end, + get_icon = config.get().opts.use_icons and function(item) + return require("mini.icons").get("file", item.v) + end or nil, + preview_item = function(item) + return { buf = vim.fn.bufadd(item) } + end, + actions = extend( + {}, + utils.make_setqflistactions(function(item) + return { filename = item.v } + end), + utils.make_fileactions(function(item) + return vim.fn.bufnr(item.v, true) + end) + ), + }, props)) end ---@class artio.picker.grep.Props : artio.picker.generic.fs.Props @@ -99,8 +98,8 @@ builtins.grep = function(props) }) return artio.pick(extend({ - items = {}, prompt = "grep", + live = true, get_items = function(input) if input == "" then return {} @@ -108,24 +107,31 @@ builtins.grep = function(props) local lines = grepcmd(input) - vim.fn.setloclist(ui2.wins.cmd, {}, " ", { - title = "grep[" .. input .. "]", - lines = lines, - efm = vim.o.grepformat, - nr = "$", - }) - - return vim - .iter(ipairs(vim.fn.getloclist(ui2.wins.cmd))) - :map(function(i, locitem) - local name = vim.fs.abspath(vim.fn.bufname(locitem.bufnr)) - return { - id = i, - v = { name, locitem.lnum, locitem.col }, - text = ("%s:%d:%d:%s"):format(vim.fs.relpath(base_dir, name), locitem.lnum, locitem.col, locitem.text), - } - end) - :totable() + local co = coroutine.running() + assert(co) + + coroutine.wrap(vim.schedule_wrap(function() + vim.fn.setloclist(ui2.wins.cmd, {}, " ", { + title = "grep[" .. input .. "]", + lines = lines, + efm = vim.o.grepformat, + nr = "$", + }) + local results = vim + .iter(ipairs(vim.fn.getloclist(ui2.wins.cmd))) + :map(function(i, locitem) + local name = vim.fs.abspath(vim.fn.bufname(locitem.bufnr)) + return { + id = i, + v = { name, locitem.lnum, locitem.col }, + text = ("%s:%d:%d:%s"):format(vim.fs.relpath(base_dir, name), locitem.lnum, locitem.col, locitem.text), + } + end) + :totable() + coroutine.resume(co, results) + end))() + + return coroutine.yield() end, fn = artio.sorter, on_close = function(item, _) @@ -336,9 +342,6 @@ builtins.smart = function(props) props.findprg = props.findprg or findprg local base_dir = vim.fn.getcwd(0) - local lst = utils.make_cmd(props.findprg, { - cwd = base_dir, - })() local recentlst = vim .iter(find_buffers()) @@ -348,33 +351,37 @@ builtins.smart = function(props) end) :totable() - local items = vim.list.unique( - vim - .iter({ recentlst, lst }) - :flatten(1) - :map(function(x) - if type(x) == "string" then - x = { path = x } - end - return x - end) - :map(function(x) - if x.buf and x.buf == currentbuf then - x.current = true - elseif x.buf and x.buf == alternatebuf then - x.alt = true - end - return x - end) - :totable(), - function(x) - return x.path - end - ) - return artio.pick(extend({ prompt = "smart", - items = items, + live = false, + get_items = function(_) + local lst = utils.make_cmd(props.findprg, { + cwd = base_dir, + })() + + local results = vim + .iter({ recentlst, lst }) + :flatten(1) + :map(function(x) + if type(x) == "string" then + x = { path = x } + end + return x + end) + :map(function(x) + if x.buf and x.buf == currentbuf then + x.current = true + elseif x.buf and x.buf == alternatebuf then + x.alt = true + end + return x + end) + :totable() + + return vim.list.unique(results, function(x) + return x.path + end) + end, fn = artio.mergesorters( "base", -- use default sorter but only display buffer files if input is empty diff --git a/lua/artio/picker.lua b/lua/artio/picker.lua index d2cff4a..d816770 100644 --- a/lua/artio/picker.lua +++ b/lua/artio/picker.lua @@ -1,4 +1,5 @@ local View = require("artio.view") +local ui2 = require("vim._core.ui2") ---@alias artio.Picker.item { id: integer, v: any, text: string, icon?: string, icon_hl?: string, hls?: artio.Picker.hl[] } ---@alias artio.Picker.match { [1]: integer, [2]: integer[], [3]: integer } item, positions, score @@ -45,7 +46,12 @@ Picker.active_picker = nil ---@param props artio.Picker.config function Picker:new(props) - vim.validate("Picker.items", props.items, "table") + vim.validate("Picker.items", props.items, function(v) + if not v then + return props.get_items and type(props.get_items) == "function" + end + return type(v) == "table" and vim.islist(v) + end) vim.validate("Picker:fn", props.fn, "function") vim.validate("Picker:on_close", props.on_close, "function") @@ -55,7 +61,6 @@ function Picker:new(props) input = nil, liveinput = nil, idx = 0, - items = {}, matches = {}, marked = {}, }, require("artio.config").get(), props) @@ -74,8 +79,6 @@ function Picker:new(props) t.liveinput = "" end - Picker.getitems(t, "") - return setmetatable(t, Picker) end @@ -93,7 +96,11 @@ function Picker:open() self.view = View:new(self) + ui2.check_targets() self.thread = coroutine.wrap(function() + -- self:getitems("") + -- self.items = self.items or {} + self.view:open() self:initkeymaps() @@ -221,8 +228,8 @@ local function item_is_structured(item) end function Picker:getitems(input) - if self.live then - self.items = self.get_items and self.get_items(input) or self.items + if (self.items == nil or self.live) and self.get_items then + self.items = self.get_items(input) end if #self.items > 0 and not item_is_structured(self.items[1]) then @@ -246,10 +253,13 @@ end ---@param input? string function Picker:getmatches(input) + assert(coroutine.running(), "Picker:getmatches needs to be called from a coroutine") + if not input then input = self.live and self.liveinput or self.input end self:getitems(input) + assert(self.items ~= nil, "Picker.items cannot be nil after Picker:getitems") -- if live, ignore sorting if self.live then diff --git a/lua/artio/utils.lua b/lua/artio/utils.lua index 75ac6d7..9c81f4f 100644 --- a/lua/artio/utils.lua +++ b/lua/artio/utils.lua @@ -1,3 +1,5 @@ +local uv = vim.uv + local utils = {} ---@param path string @@ -18,13 +20,15 @@ function utils.edit(path, ctx) return f() end -local function cmd_callback(o) - local src = o.stderr - if o.code == 0 then - src = o.stdout +local function parsechunks(chunks) + local lines = {} + + for _, chunk in ipairs(chunks) do + for _, line in ipairs(vim.split(chunk, "\n", { trimempty = true })) do + table.insert(lines, line) + end end - src = src - local lines = vim.split(src, "\n", { trimempty = true }) + return lines end @@ -32,6 +36,9 @@ end ---@param opts? table ---@return fun(arg?: string): string[] function utils.make_cmd(prg, opts) + ---@async + ---@param arg? string + ---@return string[] return function(arg) if not prg then return {} @@ -44,14 +51,41 @@ function utils.make_cmd(prg, opts) cmd = ("%s %s"):format(prg, arg) end end - return cmd_callback(vim - .system( - { vim.o.shell, vim.o.shellcmdflag, cmd }, - vim.tbl_extend("force", { - text = true, - }, opts or {}) - ) - :wait()) + + local chunks = {} + + local stdout = uv.new_pipe() + + local co = coroutine.running() + assert(co, "utils.make_cmd needs to be run inside a coroutine") + + uv.spawn( + vim.o.shell, + vim.tbl_extend("keep", { + stdio = { nil, stdout, nil }, + args = { vim.o.shellcmdflag, cmd }, + }, opts or {}), + function(code, signal) + if code == 0 then + local lines = parsechunks(chunks) + coroutine.resume(co, lines) + return + end + coroutine.resume(co, { + ("error while running shell cmd %s (%d)"):format(signal, code), + }) + end + ) + + ---@diagnostic disable-next-line: param-type-mismatch + uv.read_start(stdout, function(err, data) + assert(not err, err) + if data then + table.insert(chunks, data) + end + end) + + return coroutine.yield() end end diff --git a/lua/artio/view.lua b/lua/artio/view.lua index 0869f4c..07fea11 100644 --- a/lua/artio/view.lua +++ b/lua/artio/view.lua @@ -258,8 +258,6 @@ function View:open() _log = nil _log = {} - ui2.check_targets() - vim.schedule(function() self.augroup = vim.api.nvim_create_augroup("@artio.view", { clear = true }) @@ -553,7 +551,7 @@ local function getpromptinfo(p, info) if info == "index" then return ("[%d]"):format(p.idx) elseif info == "list" then - return ("(%d/%d)"):format(#p.matches, #p.items) + return ("(%d/%d)"):format(#p.matches, p.items and #p.items or 0) end return "" end -- 2.51.2