diff --git a/README.md b/README.md index 0cc20dcc..d57cc1e2 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,13 @@ The `:Rocks sync` command synchronizes the installed rocks with the `rocks.toml` ### Uninstalling rocks -To uninstall a rock, edit the `rocks.toml` and run `:Rocks sync`. +To uninstall a rock and any of its dependencies, +that are no longer needed, run the `:Rocks prune [rock]` command. + +> [!NOTE] +> +> - The command provides completions for rocks that can safely +> be pruned without breaking dependencies. ## :book: License diff --git a/flake.nix b/flake.nix index e1cffdd7..85aae9b0 100644 --- a/flake.nix +++ b/flake.nix @@ -134,6 +134,7 @@ editorconfig-checker ] ++ (with pkgs; [ + lua5_1 luarocks ]); }; diff --git a/lua/nio/control.lua b/lua/nio/control.lua index 8118fd70..2ec2cd20 100644 --- a/lua/nio/control.lua +++ b/lua/nio/control.lua @@ -12,8 +12,7 @@ nio.control = {} --- The event can be set from a non-async context. ---@class nio.control.Event ---@field set fun(max_woken?: integer): nil Set the event and signal to all (or limited number of) listeners that the event has occurred. If max_woken is provided and there are more listeners then the event is cleared immediately ----@field wait async fun(): nil Wait for the event to occur, returning immediately if ---- already set +---@field wait async fun(): nil Wait for the event to occur, returning immediately if already set ---@field clear fun(): nil Clear the event ---@field is_set fun(): boolean Returns true if the event is set diff --git a/lua/rocks/commands.lua b/lua/rocks/commands.lua index 4dfbd50d..2a469c09 100644 --- a/lua/rocks/commands.lua +++ b/lua/rocks/commands.lua @@ -41,6 +41,14 @@ local rocks_command_tbl = { local package, version = args[1], args[2] require("rocks.operations").add(package, version) end, + prune = function(args) + if #args == 0 then + vim.notify("Rocks prune: Called without required package argument.", vim.log.levels.ERROR) + return + end + local package = args[1] + require("rocks.operations").prune(package) + end, } local function rocks(opts) @@ -78,6 +86,12 @@ function commands.create_commands() if #rocks_list > 0 then return rocks_list end + local state = require("rocks.state") + name_query = cmdline:match("^Rocks prune%s(.*)$") + rocks_list = state.complete_removable_rocks(name_query) + if #rocks_list > 0 then + return rocks_list + end if cmdline:match("^Rocks%s+%w*$") then return vim.iter(rocks_commands) :filter(function(command) diff --git a/lua/rocks/internal-types.lua b/lua/rocks/internal-types.lua index 0688f802..dbeb7464 100644 --- a/lua/rocks/internal-types.lua +++ b/lua/rocks/internal-types.lua @@ -19,6 +19,9 @@ ---@field public name string ---@field public version string +---@class OutdatedRock: Rock +---@field public target_version string + ---@class (exact) RockDependency ---@field public name string ---@field public version? string diff --git a/lua/rocks/operations.lua b/lua/rocks/operations.lua index 1928803d..55b1c6ed 100644 --- a/lua/rocks/operations.lua +++ b/lua/rocks/operations.lua @@ -33,6 +33,7 @@ local operations = {} ---@param version? string ---@return Future operations.install = function(name, version) + state.invalidate_cache() -- TODO(vhyrro): Input checking on name and version local future = nio.control.future() local install_cmd = { @@ -67,9 +68,11 @@ operations.install = function(name, version) } end +---Removes a rock ---@param name string ---@return Future operations.remove = function(name) + state.invalidate_cache() local future = nio.control.future() local systemObj = luarocks.cli({ "remove", @@ -86,6 +89,21 @@ operations.remove = function(name) } end +---Removes a rock, and recursively removes its dependencies +---if they are no longer needed. +---@type fun(name: string) +operations.remove_recursive = nio.create(function(name) + ---@cast name string + local dependencies = state.rock_dependencies(name) + operations.remove(name).wait() + local removable_rocks = state.query_removable_rocks() + for _, dep in pairs(dependencies) do + if vim.list_contains(removable_rocks, dep.name) then + operations.remove_recursive(dep.name) + end + end +end) + --- Synchronizes the user rocks with the physical state on the current machine. --- - Installs missing rocks --- - Ensures that the correct versions are installed @@ -332,4 +350,24 @@ operations.add = function(rock_name, version) end) end +---Uninstall a rock, pruning it from rocks.toml. +---@param rock_name string +operations.prune = function(rock_name) + vim.notify("Uninstalling '" .. rock_name .. "'...") + nio.run(function() + -- TODO: Error handling + operations.remove_recursive(rock_name) + vim.schedule(function() + local config_file = fs.read_or_create(config.config_path, constants.DEFAULT_CONFIG) + local user_rocks = require("toml_edit").parse(config_file) + if not user_rocks.plugins then + return + end + user_rocks.plugins[rock_name] = nil + fs.write_file(config.config_path, "w", tostring(user_rocks)) + vim.notify("Uninstalled: " .. rock_name) + end) + end) +end + return operations diff --git a/lua/rocks/state.lua b/lua/rocks/state.lua index 99c7deed..f798ea58 100644 --- a/lua/rocks/state.lua +++ b/lua/rocks/state.lua @@ -17,11 +17,15 @@ local state = {} +---Used for completions only +---@type string[] | nil +local _removable_rock_cache = nil + local luarocks = require("rocks.luarocks") local nio = require("nio") ----@type fun(): {[string]: Rock} ---@async +---@type fun(): {[string]: Rock} state.installed_rocks = nio.create(function() ---@type {[string]: Rock} local rocks = {} @@ -48,8 +52,8 @@ state.installed_rocks = nio.create(function() return rocks end) ----@type fun(): {[string]: Rock} ---@async +---@type fun(): {[string]: OutdatedRock} state.outdated_rocks = nio.create(function() ---@type {[string]: Rock} local rocks = {} @@ -78,19 +82,24 @@ state.outdated_rocks = nio.create(function() end) ---List the dependencies of an installed Rock ----@type fun(rock:Rock): {[string]: RockDependency} ---@async +---@type fun(rock:Rock|string): {[string]: RockDependency} state.rock_dependencies = nio.create(function(rock) + ---@cast rock Rock|string + ---@type {[string]: RockDependency} local dependencies = {} local future = nio.control.future() + ---@type string + local rock_name = rock.name or rock + luarocks.cli({ "show", "--deps", "--porcelain", - rock.name, + rock_name, }, function(obj) if obj.code ~= 0 then future.set_error(obj.stderr) @@ -114,4 +123,57 @@ state.rock_dependencies = nio.create(function(rock) return dependencies end) +---List installed rocks that are not dependencies of any other rocks +---and can be removed. +---@async +---@type fun(): string[] +state.query_removable_rocks = nio.create(function() + local installed_rocks = state.installed_rocks() + --- Unfortunately, luarocks can't list dependencies via its CLI. + ---@type string[] + local dependent_rocks = {} + for _, rock in pairs(installed_rocks) do + for _, dep in pairs(state.rock_dependencies(rock)) do + dependent_rocks[#dependent_rocks + 1] = dep.name + end + end + ---@diagnostic disable-next-line: invisible + return vim.iter(nio.fn.keys(installed_rocks)) + :filter(function(rock_name) + return rock_name ~= "rocks.nvim" and not vim.list_contains(dependent_rocks, rock_name) + end) + :totable() +end) + +---@async +local populate_removable_rock_cache = nio.create(function() + if _removable_rock_cache then + return + end + _removable_rock_cache = state.query_removable_rocks() +end) + +---Completion for installed rocks that are not dependencies of other rocks +---and can be removed. +---@param query string | nil +---@return string[] +state.complete_removable_rocks = function(query) + if not _removable_rock_cache then + nio.run(populate_removable_rock_cache) + return {} + end + if not query then + return {} + end + return vim.iter(_removable_rock_cache) + :filter(function(rock_name) + return vim.startswith(rock_name, query) + end) + :totable() +end + +state.invalidate_cache = function() + _removable_rock_cache = nil +end + return state