Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: :Rocks prune command to uninstall rocks and dependencies #41

Merged
merged 1 commit into from
Dec 3, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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

1 change: 1 addition & 0 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -134,6 +134,7 @@
editorconfig-checker
]
++ (with pkgs; [
lua5_1
luarocks
]);
};
3 changes: 1 addition & 2 deletions lua/nio/control.lua
Original file line number Diff line number Diff line change
@@ -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

14 changes: 14 additions & 0 deletions lua/rocks/commands.lua
Original file line number Diff line number Diff line change
@@ -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)
3 changes: 3 additions & 0 deletions lua/rocks/internal-types.lua
Original file line number Diff line number Diff line change
@@ -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
38 changes: 38 additions & 0 deletions lua/rocks/operations.lua
Original file line number Diff line number Diff line change
@@ -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
70 changes: 66 additions & 4 deletions lua/rocks/state.lua
Original file line number Diff line number Diff line change
@@ -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