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
Show file tree
Hide file tree
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
Expand Up @@ -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

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

Expand Down
14 changes: 14 additions & 0 deletions lua/rocks/commands.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions lua/rocks/internal-types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions lua/rocks/operations.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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",
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Up @@ -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 = {}
Expand All @@ -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 = {}
Expand Down Expand Up @@ -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)
Expand All @@ -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
Loading