From 1840570905d20d5a1553b73d0ff67d93fb519374 Mon Sep 17 00:00:00 2001 From: Marc Jakobi Date: Sun, 10 Dec 2023 00:05:41 +0100 Subject: [PATCH 1/7] fix(sync): prune rocks sequentially to prevent partial uninstalls --- lua/rocks/operations.lua | 131 ++++++++++++++++++++++++--------------- 1 file changed, 80 insertions(+), 51 deletions(-) diff --git a/lua/rocks/operations.lua b/lua/rocks/operations.lua index f6b3cb72..7b2e39b4 100644 --- a/lua/rocks/operations.lua +++ b/lua/rocks/operations.lua @@ -30,10 +30,11 @@ local operations = {} ---@field wait fun() Wait in an async context. Does not block in a sync context ---@field wait_sync fun() Wait in a sync context ----@alias rock_table { [string]: Rock[] | string } +---@alias rock_config_table { [string]: Rock|string } +---@alias rock_table { [string]: Rock } ---Decode the user rocks from rocks.toml, creating a default config file if it does not exist ----@return { rocks?: rock_table, plugins?: rock_table } +---@return { rocks?: rock_config_table, plugins?: rock_config_table } local function parse_user_rocks() local config_file = fs.read_or_create(config.config_path, constants.DEFAULT_CONFIG) return require("toml_edit").parse(config_file) @@ -193,6 +194,7 @@ operations.sync = function(user_rocks) } end end + ---@cast user_rocks rock_table local installed_rocks = state.installed_rocks() @@ -202,16 +204,19 @@ operations.sync = function(user_rocks) ---@diagnostic disable-next-line: invisible local key_list = nio.fn.keys(vim.tbl_deep_extend("force", installed_rocks, user_rocks)) - local actions = vim.empty_dict() - ---@cast actions (fun():any)[] + local install_actions = vim.empty_dict() + ---@cast install_actions (fun():any)[] local ct = 1 - local dependencies = vim.empty_dict() - ---@cast dependencies {[string]: RockDependency} + local to_prune_keys = vim.empty_dict() + ---@cast to_prune_keys string[] + + local action_count - local to_remove_keys = vim.empty_dict() - ---@cast to_remove_keys string[] + local function get_progress_percentage() + return math.floor(ct / action_count * 100) + end for _, key in ipairs(key_list) do if user_rocks[key] and not installed_rocks[key] then @@ -219,7 +224,7 @@ operations.sync = function(user_rocks) progress_handle:report({ message = ("Installing: %s"):format(key), }) - table.insert(actions, function() + table.insert(install_actions, function() -- If the plugin version is a development release then we pass `dev` as the version to the install function -- as it gets converted to the `--dev` flag on there, allowing luarocks to pull the `scm-1` rockspec manifest local future @@ -237,13 +242,13 @@ operations.sync = function(user_rocks) -- TODO: Keep track of failures: #55 progress_handle:report({ message = ("Failed to install %s: %s"):format(key, vim.inspect(ret)), - percentage = math.floor(ct / #actions * 100), + percentage = get_progress_percentage(), }) return end progress_handle:report({ message = ("Installed: %s"):format(key), - percentage = math.floor(ct / #actions * 100), + percentage = get_progress_percentage(), }) return ret end) @@ -260,7 +265,7 @@ operations.sync = function(user_rocks) message = is_downgrading and ("Downgrading: %s"):format(key) or ("Updating: %s"):format(key), }) - table.insert(actions, function() + table.insert(install_actions, function() local future = operations.install(user_rocks[key].name, user_rocks[key].version) local success, ret = pcall(future.wait) @@ -269,23 +274,42 @@ operations.sync = function(user_rocks) if not success then has_errors = true progress_handle:report({ - message = ("Failed to downgrade %s: %s"):format(key, vim.inspect(ret)), - percentage = math.floor(ct / #actions * 100), + message = is_downgrading and ("Failed to downgrade %s: %s"):format(key, vim.inspect(ret)) + or ("Failed to upgrade %s: %s"):format(key, vim.inspect(ret)), + percentage = get_progress_percentage(), }) return end progress_handle:report({ message = is_downgrading and ("Downgraded: %s"):format(key) or ("Upgraded: %s"):format(key), - percentage = math.floor(ct / #actions * 100), + percentage = get_progress_percentage(), }) return ret end) elseif not user_rocks[key] and installed_rocks[key] then - table.insert(to_remove_keys, key) + table.insert(to_prune_keys, key) end + end - if installed_rocks[key] then + -- For now, we estimate assuming all values to prune + -- can be pruned + action_count = #install_actions + #to_prune_keys + + -- Run install actions before removals, to make sure they don't conflict + -- TODO: Error handling + if not vim.tbl_isempty(install_actions) then + nio.gather(install_actions) + end + + -- Determine dependencies of installed user rocks, so they can be excluded from rocks to prune + -- NOTE(mrcjkb): This has to be done after installation, + -- so that we don't prune dependencies of newly installed rocks. + installed_rocks = state.installed_rocks() + local dependencies = vim.empty_dict() + ---@cast dependencies {[string]: RockDependency} + for _, key in ipairs(key_list) do + if user_rocks[key] and installed_rocks[key] then -- NOTE(vhyrro): It is not possible to use the vim.tbl_extend or vim.tbl_deep_extend -- functions here within the async context. It simply refuses to work. for k, v in pairs(state.rock_dependencies(installed_rocks[key])) do @@ -294,44 +318,50 @@ operations.sync = function(user_rocks) end end - for _, key in ipairs(to_remove_keys) do - local is_dependency = dependencies[key] ~= nil - if not is_dependency then - nio.scheduler() - progress_handle:report({ - message = ("Removing: %s"):format(key), - }) + ---@type string[] + local rocks_to_prune = vim.iter(to_prune_keys) + :filter(function(key) + return dependencies[key] == nil + end) + :totable() - table.insert(actions, function() - local future = operations.remove(installed_rocks[key].name) - local success, ret = pcall(future.wait) + if vim.tbl_isempty(install_actions) and vim.tbl_isempty(rocks_to_prune) then + nio.scheduler() + progress_handle:report({ message = "Everything is in-sync!", percentage = 100 }) + progress_handle:finish() + return + end - ct = ct + 1 - nio.scheduler() - if not success then - has_errors = true - -- TODO: Keep track of failures: #55 - progress_handle:report({ - message = ("Failed to install %s: %s"):format(key, vim.inspect(ret)), - percentage = math.floor(ct / #actions * 100), - }) - return - end - progress_handle:report({ - message = ("Removed: %s"):format(key), - percentage = math.floor(ct / #actions * 100), - }) - return ret - end) + action_count = #install_actions + #rocks_to_prune + + ---@diagnostic disable-next-line: invisible + local user_rock_names = nio.fn.keys(user_rocks) + -- Prune rocks sequentially, to prevent conflicts + for _, key in ipairs(rocks_to_prune) do + nio.scheduler() + progress_handle:report({ + message = ("Removing: %s"):format(key), + }) + + local success = operations.remove_recursive(installed_rocks[key].name, user_rock_names) + + ct = ct + 1 + nio.scheduler() + if not success then + has_errors = true + -- TODO: Keep track of failures: #55 + progress_handle:report({ + message = ("Failed to prune %s"):format(key), + percentage = get_progress_percentage(), + }) + else + progress_handle:report({ + message = ("Removed: %s"):format(key), + percentage = get_progress_percentage(), + }) end end - if not vim.tbl_isempty(actions) then - -- TODO: Error handling - nio.gather(actions) - else - progress_handle:report({ message = "Everything is in-sync!", percentage = 100 }) - end if has_errors then progress_handle:report({ title = "Error", @@ -393,7 +423,6 @@ operations.update = function() if not vim.tbl_isempty(actions) then nio.gather(actions) - fs.write_file(config.config_path, "w", tostring(user_rocks)) else nio.scheduler() progress_handle:report({ message = "Nothing to update!", percentage = 100 }) From 95ec02acd9c590b341ce01ac5ee8da160e9041d9 Mon Sep 17 00:00:00 2001 From: Marc Jakobi Date: Sun, 10 Dec 2023 23:48:18 +0100 Subject: [PATCH 2/7] fix(sync): prevent luarocks race conditions --- lua/rocks/luarocks.lua | 25 ++++++++++++++++++++++--- lua/rocks/state.lua | 10 +++++++--- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/lua/rocks/luarocks.lua b/lua/rocks/luarocks.lua index 6b855133..6ca78cf0 100644 --- a/lua/rocks/luarocks.lua +++ b/lua/rocks/luarocks.lua @@ -21,20 +21,39 @@ local constants = require("rocks.constants") local config = require("rocks.config.internal") local nio = require("nio") +---@class LuarocksCliOpts: SystemOpts +---@field synchronized? boolean Whether to wait for and acquire a lock (recommended for file system IO, default: `true`) + +local lock = nio.control.future() +lock.set(true) -- initialise as unlocked + ---@param args string[] luarocks CLI arguments ---@param on_exit (function|nil) Called asynchronously when the luarocks command exits. --- asynchronously. Receives SystemCompleted object, see return of SystemObj:wait(). ----@param opts? SystemOpts +---@param opts? LuarocksCliOpts ---@return vim.SystemObj ---@see vim.system luarocks.cli = function(args, on_exit, opts) + opts = opts or {} + opts.synchronized = opts.synchronized ~= nil and opts.synchronized or false + local on_exit_wrapped = on_exit and vim.schedule_wrap(on_exit) + if opts.synchronized then + lock.wait() + lock = nio.control.future() + on_exit_wrapped = vim.schedule_wrap(function(...) + pcall(lock.set, true) + if on_exit then + on_exit(...) + end + end) + end local luarocks_cmd = vim.list_extend({ config.luarocks_binary, "--lua-version=" .. constants.LUA_VERSION, "--tree=" .. config.rocks_path, "--server='https://nvim-neorocks.github.io/rocks-binaries/'", }, args) - return vim.system(luarocks_cmd, opts, on_exit and vim.schedule_wrap(on_exit)) + return vim.system(luarocks_cmd, opts, on_exit_wrapped) end ---Search luarocks.org for all packages. @@ -46,7 +65,7 @@ luarocks.search_all = nio.create(function(callback) luarocks.cli({ "search", "--porcelain", "--all" }, function(obj) ---@cast obj vim.SystemCompleted future.set(obj) - end, { text = true }) + end, { text = true, synchronized = false }) ---@type vim.SystemCompleted local obj = future.wait() local result = obj.stdout diff --git a/lua/rocks/state.lua b/lua/rocks/state.lua index d29f12fe..59bb221e 100644 --- a/lua/rocks/state.lua +++ b/lua/rocks/state.lua @@ -101,15 +101,19 @@ state.rock_dependencies = nio.create(function(rock) rock_name, }, function(obj) if obj.code ~= 0 then - future.set_error(obj.stderr) + future.set_error(("Could not get dependencies for rock %s: %s"):format(rock_name, obj.stderr)) else future.set(obj.stdout) end end, { text = true }) - local dependency_list = future.wait() + local success, result = pcall(future.wait) + if not success then + -- TODO: Log error + return {} + end - for line in string.gmatch(dependency_list, "%S*[^\n]+") do + for line in string.gmatch(result, "%S*[^\n]+") do local name, version = line:match("(%S+)%s%S+%s(%S+)") if not name then name = line:match("(%S+)") From 5144b710720c193e18c274ef35632f7dccb11926 Mon Sep 17 00:00:00 2001 From: Marc Jakobi Date: Sun, 10 Dec 2023 23:55:16 +0100 Subject: [PATCH 3/7] refactor(sync): sequential installs and up/downgrades --- lua/rocks/operations.lua | 102 +++++++++++++++------------------------ 1 file changed, 38 insertions(+), 64 deletions(-) diff --git a/lua/rocks/operations.lua b/lua/rocks/operations.lua index 7b2e39b4..097d0e21 100644 --- a/lua/rocks/operations.lua +++ b/lua/rocks/operations.lua @@ -204,18 +204,13 @@ operations.sync = function(user_rocks) ---@diagnostic disable-next-line: invisible local key_list = nio.fn.keys(vim.tbl_deep_extend("force", installed_rocks, user_rocks)) - local install_actions = vim.empty_dict() - ---@cast install_actions (fun():any)[] - local ct = 1 local to_prune_keys = vim.empty_dict() ---@cast to_prune_keys string[] - local action_count - local function get_progress_percentage() - return math.floor(ct / action_count * 100) + return math.floor(ct / #key_list * 100) end for _, key in ipairs(key_list) do @@ -224,34 +219,30 @@ operations.sync = function(user_rocks) progress_handle:report({ message = ("Installing: %s"):format(key), }) - table.insert(install_actions, function() - -- If the plugin version is a development release then we pass `dev` as the version to the install function - -- as it gets converted to the `--dev` flag on there, allowing luarocks to pull the `scm-1` rockspec manifest - local future - if vim.startswith(user_rocks[key].version, "scm-") then - future = operations.install(user_rocks[key].name, "dev") - else - future = operations.install(user_rocks[key].name, user_rocks[key].version) - end - local success, ret = pcall(future.wait) - - ct = ct + 1 - nio.scheduler() - if not success then - has_errors = true - -- TODO: Keep track of failures: #55 - progress_handle:report({ - message = ("Failed to install %s: %s"):format(key, vim.inspect(ret)), - percentage = get_progress_percentage(), - }) - return - end + -- If the plugin version is a development release then we pass `dev` as the version to the install function + -- as it gets converted to the `--dev` flag on there, allowing luarocks to pull the `scm-1` rockspec manifest + local future + if vim.startswith(user_rocks[key].version, "scm-") then + future = operations.install(user_rocks[key].name, "dev") + else + future = operations.install(user_rocks[key].name, user_rocks[key].version) + end + local success, ret = pcall(future.wait) + + ct = ct + 1 + nio.scheduler() + if not success then + has_errors = true + -- TODO: Keep track of failures: #55 progress_handle:report({ - message = ("Installed: %s"):format(key), + message = ("Failed to install %s: %s"):format(key, vim.inspect(ret)), percentage = get_progress_percentage(), }) - return ret - end) + end + progress_handle:report({ + message = ("Installed: %s"):format(key), + percentage = get_progress_percentage(), + }) elseif user_rocks[key] and installed_rocks[key] @@ -265,43 +256,28 @@ operations.sync = function(user_rocks) message = is_downgrading and ("Downgrading: %s"):format(key) or ("Updating: %s"):format(key), }) - table.insert(install_actions, function() - local future = operations.install(user_rocks[key].name, user_rocks[key].version) - local success, ret = pcall(future.wait) - - ct = ct + 1 - nio.scheduler() - if not success then - has_errors = true - progress_handle:report({ - message = is_downgrading and ("Failed to downgrade %s: %s"):format(key, vim.inspect(ret)) - or ("Failed to upgrade %s: %s"):format(key, vim.inspect(ret)), - percentage = get_progress_percentage(), - }) - return - end + local future = operations.install(user_rocks[key].name, user_rocks[key].version) + local success, ret = pcall(future.wait) + + ct = ct + 1 + nio.scheduler() + if not success then + has_errors = true progress_handle:report({ - message = is_downgrading and ("Downgraded: %s"):format(key) or ("Upgraded: %s"):format(key), + message = is_downgrading and ("Failed to downgrade %s: %s"):format(key, vim.inspect(ret)) + or ("Failed to upgrade %s: %s"):format(key, vim.inspect(ret)), percentage = get_progress_percentage(), }) - - return ret - end) + end + progress_handle:report({ + message = is_downgrading and ("Downgraded: %s"):format(key) or ("Upgraded: %s"):format(key), + percentage = get_progress_percentage(), + }) elseif not user_rocks[key] and installed_rocks[key] then table.insert(to_prune_keys, key) end end - -- For now, we estimate assuming all values to prune - -- can be pruned - action_count = #install_actions + #to_prune_keys - - -- Run install actions before removals, to make sure they don't conflict - -- TODO: Error handling - if not vim.tbl_isempty(install_actions) then - nio.gather(install_actions) - end - -- Determine dependencies of installed user rocks, so they can be excluded from rocks to prune -- NOTE(mrcjkb): This has to be done after installation, -- so that we don't prune dependencies of newly installed rocks. @@ -325,15 +301,13 @@ operations.sync = function(user_rocks) end) :totable() - if vim.tbl_isempty(install_actions) and vim.tbl_isempty(rocks_to_prune) then + if ct == 0 and vim.tbl_isempty(rocks_to_prune) then nio.scheduler() progress_handle:report({ message = "Everything is in-sync!", percentage = 100 }) progress_handle:finish() return end - action_count = #install_actions + #rocks_to_prune - ---@diagnostic disable-next-line: invisible local user_rock_names = nio.fn.keys(user_rocks) -- Prune rocks sequentially, to prevent conflicts @@ -351,7 +325,7 @@ operations.sync = function(user_rocks) has_errors = true -- TODO: Keep track of failures: #55 progress_handle:report({ - message = ("Failed to prune %s"):format(key), + message = ("Failed to remove %s"):format(key), percentage = get_progress_percentage(), }) else From aa49931339347e46ef1a63815ac94ead6f5ee6ca Mon Sep 17 00:00:00 2001 From: Marc Jakobi Date: Mon, 11 Dec 2023 00:08:32 +0100 Subject: [PATCH 4/7] feat(sync): separate progress handles for errors --- lua/rocks/operations.lua | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/lua/rocks/operations.lua b/lua/rocks/operations.lua index 097d0e21..4f1ca7ed 100644 --- a/lua/rocks/operations.lua +++ b/lua/rocks/operations.lua @@ -168,13 +168,26 @@ end) ---@param user_rocks? { [string]: Rock|string } loaded from rocks.toml if `nil` operations.sync = function(user_rocks) nio.run(function() - local has_errors = false local progress_handle = progress.handle.create({ title = "Syncing", lsp_client = { name = constants.ROCKS_NVIM }, percentage = 0, }) + ---@type ProgressHandle[] + local error_handles = {} + ---@param message string + local function report_error(message) + table.insert( + error_handles, + progress.handle.create({ + title = "Error", + lsp_client = { name = constants.ROCKS_NVIM }, + message = message, + }) + ) + end + if user_rocks == nil then -- Read or create a new config file and decode it -- NOTE: This does not use parse_user_rocks because we decode with toml, not toml-edit @@ -232,12 +245,11 @@ operations.sync = function(user_rocks) ct = ct + 1 nio.scheduler() if not success then - has_errors = true -- TODO: Keep track of failures: #55 progress_handle:report({ - message = ("Failed to install %s: %s"):format(key, vim.inspect(ret)), percentage = get_progress_percentage(), }) + report_error(("Failed to install %s: %s"):format(key, vim.inspect(ret))) end progress_handle:report({ message = ("Installed: %s"):format(key), @@ -262,12 +274,13 @@ operations.sync = function(user_rocks) ct = ct + 1 nio.scheduler() if not success then - has_errors = true progress_handle:report({ - message = is_downgrading and ("Failed to downgrade %s: %s"):format(key, vim.inspect(ret)) - or ("Failed to upgrade %s: %s"):format(key, vim.inspect(ret)), percentage = get_progress_percentage(), }) + report_error( + is_downgrading and ("Failed to downgrade %s: %s"):format(key, vim.inspect(ret)) + or ("Failed to upgrade %s: %s"):format(key, vim.inspect(ret)) + ) end progress_handle:report({ message = is_downgrading and ("Downgraded: %s"):format(key) or ("Upgraded: %s"):format(key), @@ -322,12 +335,11 @@ operations.sync = function(user_rocks) ct = ct + 1 nio.scheduler() if not success then - has_errors = true -- TODO: Keep track of failures: #55 progress_handle:report({ - message = ("Failed to remove %s"):format(key), percentage = get_progress_percentage(), }) + report_error(("Failed to remove %s"):format(key)) else progress_handle:report({ message = ("Removed: %s"):format(key), @@ -336,13 +348,16 @@ operations.sync = function(user_rocks) end end - if has_errors then + if not vim.tbl_isempty(error_handles) then progress_handle:report({ title = "Error", message = "Sync completed with errors!", percentage = 100, }) progress_handle:cancel() + for _, error_handle in pairs(error_handles) do + error_handle:cancel() + end else progress_handle:finish() end From 0ab216720954d25cc8e1fbbb83425e093a0fb8e3 Mon Sep 17 00:00:00 2001 From: Marc Jakobi Date: Mon, 11 Dec 2023 00:32:09 +0100 Subject: [PATCH 5/7] fix(sync): don't try to remove indirect dependencies --- lua/rocks/operations.lua | 12 +++++------- lua/rocks/state.lua | 5 ++--- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/lua/rocks/operations.lua b/lua/rocks/operations.lua index 4f1ca7ed..813e8407 100644 --- a/lua/rocks/operations.lua +++ b/lua/rocks/operations.lua @@ -294,16 +294,14 @@ operations.sync = function(user_rocks) -- Determine dependencies of installed user rocks, so they can be excluded from rocks to prune -- NOTE(mrcjkb): This has to be done after installation, -- so that we don't prune dependencies of newly installed rocks. + -- TODO: This doesn't guarantee that all rocks that can be pruned will be pruned. + -- Typically, another sync will fix this. Maybe we should do some sort of repeat... until? installed_rocks = state.installed_rocks() local dependencies = vim.empty_dict() ---@cast dependencies {[string]: RockDependency} - for _, key in ipairs(key_list) do - if user_rocks[key] and installed_rocks[key] then - -- NOTE(vhyrro): It is not possible to use the vim.tbl_extend or vim.tbl_deep_extend - -- functions here within the async context. It simply refuses to work. - for k, v in pairs(state.rock_dependencies(installed_rocks[key])) do - dependencies[k] = v - end + for _, installed_rock in pairs(installed_rocks) do + for k, v in pairs(state.rock_dependencies(installed_rock)) do + dependencies[k] = v end end diff --git a/lua/rocks/state.lua b/lua/rocks/state.lua index 59bb221e..0af61222 100644 --- a/lua/rocks/state.lua +++ b/lua/rocks/state.lua @@ -96,7 +96,6 @@ state.rock_dependencies = nio.create(function(rock) luarocks.cli({ "show", - "--deps", "--porcelain", rock_name, }, function(obj) @@ -114,7 +113,7 @@ state.rock_dependencies = nio.create(function(rock) end for line in string.gmatch(result, "%S*[^\n]+") do - local name, version = line:match("(%S+)%s%S+%s(%S+)") + local name, version = line:match("dependency%s+(%S+).*using%s+([^-%s]+)") if not name then name = line:match("(%S+)") end @@ -136,7 +135,7 @@ state.query_removable_rocks = nio.create(function() ---@cast dependent_rocks string[] for _, rock in pairs(installed_rocks) do for _, dep in pairs(state.rock_dependencies(rock)) do - dependent_rocks[#dependent_rocks + 1] = dep.name + table.insert(dependent_rocks, dep.name) end end ---@diagnostic disable-next-line: invisible From f051a94d63f929525f7edd7f86bfded8b79cb887 Mon Sep 17 00:00:00 2001 From: Marc Jakobi Date: Mon, 11 Dec 2023 01:23:40 +0100 Subject: [PATCH 6/7] docs(prune): add note about sync not always pruning all rocks --- lua/rocks/commands.lua | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lua/rocks/commands.lua b/lua/rocks/commands.lua index 0a628fcd..525baec6 100644 --- a/lua/rocks/commands.lua +++ b/lua/rocks/commands.lua @@ -7,12 +7,13 @@ --- command action --------------------------------------------------------------------------------- --- ---- install [rock] [version?] install {rock} with optional {version}. ---- prune [rock] uninstall {rock} and its stale dependencies, +--- install [rock] [version?] Install {rock} with optional {version}. +--- prune [rock] Uninstall {rock} and its stale dependencies, --- and remove it from rocks.toml. ---- sync synchronize installed rocks with rocks.toml. ---- update search for updated rocks and install them. ---- edit edit the rocks.toml file. +--- sync Synchronize installed rocks with rocks.toml. +--- It may take more than one sync to prune all rocks that can be pruned. +--- update Search for updated rocks and install them. +--- edit Edit the rocks.toml file. --- ---@brief ]] --- From c1b856952bfcbfce1c05659f0540f567133cfaf1 Mon Sep 17 00:00:00 2001 From: Marc Jakobi Date: Mon, 11 Dec 2023 01:36:54 +0100 Subject: [PATCH 7/7] fix(ui): sync progress percentage computation --- lua/rocks/operations.lua | 131 +++++++++++++++++++++------------------ 1 file changed, 72 insertions(+), 59 deletions(-) diff --git a/lua/rocks/operations.lua b/lua/rocks/operations.lua index 813e8407..a90fbf68 100644 --- a/lua/rocks/operations.lua +++ b/lua/rocks/operations.lua @@ -217,78 +217,89 @@ operations.sync = function(user_rocks) ---@diagnostic disable-next-line: invisible local key_list = nio.fn.keys(vim.tbl_deep_extend("force", installed_rocks, user_rocks)) - local ct = 1 - - local to_prune_keys = vim.empty_dict() - ---@cast to_prune_keys string[] - - local function get_progress_percentage() - return math.floor(ct / #key_list * 100) - end - + local to_install = vim.empty_dict() + ---@cast to_install string[] + local to_updowngrade = vim.empty_dict() + ---@cast to_updowngrade string[] + local to_prune = vim.empty_dict() + ---@cast to_prune string[] for _, key in ipairs(key_list) do if user_rocks[key] and not installed_rocks[key] then - nio.scheduler() - progress_handle:report({ - message = ("Installing: %s"):format(key), - }) - -- If the plugin version is a development release then we pass `dev` as the version to the install function - -- as it gets converted to the `--dev` flag on there, allowing luarocks to pull the `scm-1` rockspec manifest - local future - if vim.startswith(user_rocks[key].version, "scm-") then - future = operations.install(user_rocks[key].name, "dev") - else - future = operations.install(user_rocks[key].name, user_rocks[key].version) - end - local success, ret = pcall(future.wait) - - ct = ct + 1 - nio.scheduler() - if not success then - -- TODO: Keep track of failures: #55 - progress_handle:report({ - percentage = get_progress_percentage(), - }) - report_error(("Failed to install %s: %s"):format(key, vim.inspect(ret))) - end - progress_handle:report({ - message = ("Installed: %s"):format(key), - percentage = get_progress_percentage(), - }) + table.insert(to_install, key) elseif user_rocks[key] and installed_rocks[key] and user_rocks[key].version ~= installed_rocks[key].version then - local is_downgrading = vim.version.parse(user_rocks[key].version) - < vim.version.parse(installed_rocks[key].version) + table.insert(to_updowngrade, key) + elseif not user_rocks[key] and installed_rocks[key] then + table.insert(to_prune, key) + end + end - nio.scheduler() + local ct = 1 + local action_count = #to_install + #to_updowngrade + #to_prune + + local function get_progress_percentage() + return math.floor(ct / action_count * 100) + end + + for _, key in ipairs(to_install) do + nio.scheduler() + progress_handle:report({ + message = ("Installing: %s"):format(key), + }) + -- If the plugin version is a development release then we pass `dev` as the version to the install function + -- as it gets converted to the `--dev` flag on there, allowing luarocks to pull the `scm-1` rockspec manifest + local future + if vim.startswith(user_rocks[key].version, "scm-") then + future = operations.install(user_rocks[key].name, "dev") + else + future = operations.install(user_rocks[key].name, user_rocks[key].version) + end + local success, ret = pcall(future.wait) + + ct = ct + 1 + nio.scheduler() + if not success then + -- TODO: Keep track of failures: #55 progress_handle:report({ - message = is_downgrading and ("Downgrading: %s"):format(key) or ("Updating: %s"):format(key), + percentage = get_progress_percentage(), }) + report_error(("Failed to install %s: %s"):format(key, vim.inspect(ret))) + end + progress_handle:report({ + message = ("Installed: %s"):format(key), + percentage = get_progress_percentage(), + }) + end + for _, key in ipairs(to_updowngrade) do + local is_downgrading = vim.version.parse(user_rocks[key].version) + < vim.version.parse(installed_rocks[key].version) - local future = operations.install(user_rocks[key].name, user_rocks[key].version) - local success, ret = pcall(future.wait) + nio.scheduler() + progress_handle:report({ + message = is_downgrading and ("Downgrading: %s"):format(key) or ("Updating: %s"):format(key), + }) - ct = ct + 1 - nio.scheduler() - if not success then - progress_handle:report({ - percentage = get_progress_percentage(), - }) - report_error( - is_downgrading and ("Failed to downgrade %s: %s"):format(key, vim.inspect(ret)) - or ("Failed to upgrade %s: %s"):format(key, vim.inspect(ret)) - ) - end + local future = operations.install(user_rocks[key].name, user_rocks[key].version) + local success, ret = pcall(future.wait) + + ct = ct + 1 + nio.scheduler() + if not success then progress_handle:report({ - message = is_downgrading and ("Downgraded: %s"):format(key) or ("Upgraded: %s"):format(key), percentage = get_progress_percentage(), }) - elseif not user_rocks[key] and installed_rocks[key] then - table.insert(to_prune_keys, key) + report_error( + is_downgrading and ("Failed to downgrade %s: %s"):format(key, vim.inspect(ret)) + or ("Failed to upgrade %s: %s"):format(key, vim.inspect(ret)) + ) end + progress_handle:report({ + message = is_downgrading and ("Downgraded: %s"):format(key) or ("Upgraded: %s"):format(key), + percentage = get_progress_percentage(), + }) end -- Determine dependencies of installed user rocks, so they can be excluded from rocks to prune @@ -306,13 +317,15 @@ operations.sync = function(user_rocks) end ---@type string[] - local rocks_to_prune = vim.iter(to_prune_keys) + local prunable_rocks = vim.iter(to_prune) :filter(function(key) return dependencies[key] == nil end) :totable() - if ct == 0 and vim.tbl_isempty(rocks_to_prune) then + action_count = #to_install + #to_updowngrade + #prunable_rocks + + if ct == 0 and vim.tbl_isempty(prunable_rocks) then nio.scheduler() progress_handle:report({ message = "Everything is in-sync!", percentage = 100 }) progress_handle:finish() @@ -322,7 +335,7 @@ operations.sync = function(user_rocks) ---@diagnostic disable-next-line: invisible local user_rock_names = nio.fn.keys(user_rocks) -- Prune rocks sequentially, to prevent conflicts - for _, key in ipairs(rocks_to_prune) do + for _, key in ipairs(prunable_rocks) do nio.scheduler() progress_handle:report({ message = ("Removing: %s"):format(key),