Skip to content

Commit

Permalink
feat: Add support for config imports
Browse files Browse the repository at this point in the history
  • Loading branch information
simifalaye committed Oct 19, 2024
1 parent 9762134 commit 749d412
Show file tree
Hide file tree
Showing 9 changed files with 209 additions and 30 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,23 @@ You can also pin/unpin installed plugins with:
:Rocks [pin|unpin] {rock}
```

### Importing rocks toml files

You can break up your rocks configuration into different modules that
can then be imported into your main configuration. This can be useful
for modularity or simply for the purpose of supporting local
configuration files that you can keep outside of version control.

For example:
```toml
import = [
"rocks-local.toml", # Paths are relative to the rocks.toml file directory by default
"~/my-rocks.toml", # Path expansion is supported through vim.fn.expand
"/home/user/my-rocks.toml", # Absolute paths are supported
]

```

## :calendar: User events

For `:h User` events that rocks.nvim will trigger, see `:h rocks.user-event`.
Expand Down
69 changes: 56 additions & 13 deletions lua/rocks/config/internal.lua
Original file line number Diff line number Diff line change
Expand Up @@ -70,25 +70,68 @@ local default_config = {
---@type string[]
unrecognized_configs = {},
},
---@type fun(path: string): string[]
get_imports = function(path)
local config_file = fs.read_or_create(path, constants.DEFAULT_CONFIG)
local rocks_toml = require("toml_edit").parse_as_tbl(config_file)
if rocks_toml.import ~= nil and type(rocks_toml.import) == "table" then
return rocks_toml.import
end
return {}
end,
---@type fun(path_str: string): string
get_rocks_toml_dir = function(path_str)
local path = fs.expand_path(path_str)
if path:sub(1, 1) ~= "/" then
path = vim.fs.joinpath(fs.dirname(config.config_path), path)
end
return path
end,
---@type fun():RocksToml
get_rocks_toml = function()
local config_file = fs.read_or_create(config.config_path, constants.DEFAULT_CONFIG)
local rocks_toml = require("toml_edit").parse_as_tbl(config_file)
for key, tbl in pairs(rocks_toml) do
if key == "rocks" or key == "plugins" then
for name, data in pairs(tbl) do
if type(data) == "string" then
---@type RockSpec
rocks_toml[key][name] = {
name = name,
version = data,
}
else
rocks_toml[key][name].name = name
local rocks_toml = {}
local visited = {}

local function parse(path_str, default)
local file_path = config.get_rocks_toml_dir(path_str)
-- Don't allow recursive includes
if visited[file_path] then
return nil
end
visited[file_path] = true

local config_file = fs.read_or_create(file_path, default)
local rt = require("toml_edit").parse_as_tbl(config_file)

-- Setup rockspec for rocks/plugins
for key, tbl in pairs(rt) do
if key == "rocks" or key == "plugins" then
for name, data in pairs(tbl) do
if type(data) == "string" then
---@type RockSpec
rt[key][name] = {
name = name,
version = data,
}
else
rt[key][name].name = name
end
end
end
end
-- Merge, giving preference to existing config before imports
rocks_toml = vim.tbl_deep_extend("keep", rocks_toml, rt)

-- Follow import paths
if rt.import then
for _, path in ipairs(rt.import) do
parse(path, "")
end
end
end
parse(config.config_path, constants.DEFAULT_CONFIG)
rocks_toml.import = nil -- Remove import since we merged

return rocks_toml
end,
---@return server_url[]
Expand Down
20 changes: 20 additions & 0 deletions lua/rocks/fs.lua
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,26 @@ function fs.file_exists(location)
return false
end

--- Expand environment variables and tilde in the path string
---@param path_str string
---@return string
function fs.expand_path(path_str)
-- Expand environment variables
local path = path_str:gsub("%$([%w_]+)", function(var)
return os.getenv(var) or ""
end)
-- Expand tilde to home directory
path = path:gsub("^~", os.getenv("HOME"))
return path
end

--- Get the directory of a file path
---@param path string
---@return string
function fs.dirname(path)
return path:match("^(.*[/\\])") or "."
end

--- Write `contents` to a file asynchronously
---@param location string file path
---@param mode string mode to open the file for
Expand Down
7 changes: 3 additions & 4 deletions lua/rocks/operations/add.lua
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ local add = {}

local constants = require("rocks.constants")
local log = require("rocks.log")
local fs = require("rocks.fs")
local config = require("rocks.config.internal")
local cache = require("rocks.cache")
local helpers = require("rocks.operations.helpers")
local handlers = require("rocks.operations.handlers")
Expand Down Expand Up @@ -89,6 +87,7 @@ add.add = function(arg_list, callback, opts)

nio.run(function()
helpers.semaphore.with(function()
---@type MutRocksTomlRef
local user_rocks = helpers.parse_rocks_toml()
local handler = handlers.get_install_handler_callback(user_rocks, arg_list)
if type(handler) == "function" then
Expand All @@ -98,7 +97,7 @@ add.add = function(arg_list, callback, opts)
})
end
handler(report_progress, report_error, helpers.manage_rock_stub)
fs.write_file_await(config.config_path, "w", tostring(user_rocks))
user_rocks:write()
nio.scheduler()
progress_handle:finish()
return
Expand Down Expand Up @@ -217,7 +216,7 @@ Use 'Rocks %s {rock_name}' or install rocks-git.nvim.
else
user_rocks.plugins[rock_name] = installed_rock.version
end
fs.write_file_await(config.config_path, "w", tostring(user_rocks))
user_rocks:write()
cache.populate_all_rocks_state_caches()
vim.schedule(function()
helpers.postInstall()
Expand Down
108 changes: 105 additions & 3 deletions lua/rocks/operations/helpers.lua
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,113 @@ local helpers = {}

helpers.semaphore = nio.control.semaphore(1)

---@class MultiMutRocksTomlWrapper
---@field cache table<string, MultiMutRocksTomlWrapper> Cache for nested metatables
---@field tables MutRocksTomlRef[] Rocks toml config metatables
---@field paths? string[] File paths associated the rocks toml configs
local MultiMutRocksTomlWrapper = {}
MultiMutRocksTomlWrapper.__index = function(self, key)
-- Give preference to class methods/fields
if MultiMutRocksTomlWrapper[key] then
return MultiMutRocksTomlWrapper[key]
end
-- Find the key within the config tables
local tables = {}
for _, tbl in ipairs(self.tables) do
if tbl[key] ~= nil then
if type(tbl[key]) == "table" then
table.insert(tables, tbl[key])
else
return tbl[key]
end
end
end
-- If the value is a table, setup a nested metatable that uses the
-- inner tables of the config tables
if #tables > 0 then
if not self.cache[key] then
self.cache[key] = MultiMutRocksTomlWrapper.new(true, tables)
end
return self.cache[key]
end
return nil
end
MultiMutRocksTomlWrapper.__newindex = function(self, key, value)
for _, tbl in ipairs(self.tables) do
if tbl[key] ~= nil then
tbl[key] = value
return
end
end
-- If key not found in any table, add it to the first table
self.tables[1][key] = value
end

--- Function to create a new wrapper
---@param nested boolean Whether this is a nested metatable
---@param tables MutRocksTomlRef[] Rocks toml config metatables
---@param paths? string[] File paths associated the rocks toml configs
---@return MultiMutRocksTomlWrapper
function MultiMutRocksTomlWrapper.new(nested, tables, paths)
if #tables < 1 then
return {}
end
if not nested and #tables ~= #paths then
return {}
end
local self = { cache = {}, tables = tables, paths = paths }
setmetatable(self, MultiMutRocksTomlWrapper)
return self
end

--- Write to all rocks toml config files
---@param self MultiMutRocksTomlWrapper
MultiMutRocksTomlWrapper.write = nio.create(function(self)
if not self.paths then
return
end
for i, path in ipairs(self.paths) do
if self.tables[i] then
fs.write_file_await(path, "w", tostring(self.tables[i]))
end
end
end)

---Decode the user rocks from rocks.toml, creating a default config file if it does not exist
---@return MutRocksTomlRef
---@return MultiMutRocksTomlWrapper
function helpers.parse_rocks_toml()
local config_file = fs.read_or_create(config.config_path, constants.DEFAULT_CONFIG)
return require("toml_edit").parse(config_file)
local rocks_tomls = {}
local paths = {}
local visited = {}

local function parse(path_str, default)
local file_path = config.get_rocks_toml_dir(path_str)
-- Don't allow recursive includes
if visited[file_path] then
return
end
visited[file_path] = true

local config_file = fs.read_or_create(file_path, default)
local rocks_toml = require("toml_edit").parse(config_file)
table.insert(rocks_tomls, rocks_toml)
table.insert(paths, file_path)

-- Follow import paths
-- TODO: Remove call to get_imports, this is needed because toml-edit doesn't
-- seem to support lists. You would expect it would produce a metatable that is
-- indexed by an integer for each list item but this was not working so we need
-- to re-read the imports using `parse_as_table`
local imports = config.get_imports(file_path)
if imports then
for _, path in ipairs(imports) do
parse(path, "")
end
end
end
parse(config.config_path, constants.DEFAULT_CONFIG)

return MultiMutRocksTomlWrapper.new(false, rocks_tomls, paths)
end

---@param rocks_toml MutRocksTomlRef
Expand Down
5 changes: 2 additions & 3 deletions lua/rocks/operations/pin.lua
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,14 @@

local pin = {}

local fs = require("rocks.fs")
local config = require("rocks.config.internal")
local helpers = require("rocks.operations.helpers")
local nio = require("nio")

---@param rock_name rock_name
pin.pin = function(rock_name)
nio.run(function()
helpers.semaphore.with(function()
---@type MutRocksTomlRef
local user_config = helpers.parse_rocks_toml()
local rocks_key, user_rock = helpers.get_rock_and_key(user_config, rock_name)
if not rocks_key then
Expand All @@ -40,7 +39,7 @@ pin.pin = function(rock_name)
end
user_config[rocks_key][rock_name].pin = true
local version = user_config[rocks_key][rock_name].version
fs.write_file_await(config.config_path, "w", tostring(user_config))
user_config:write()
vim.schedule(function()
vim.notify(("%s pinned to version %s"):format(rock_name, version), vim.log.levels.INFO)
end)
Expand Down
4 changes: 2 additions & 2 deletions lua/rocks/operations/prune.lua
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ local prune = {}

local constants = require("rocks.constants")
local log = require("rocks.log")
local fs = require("rocks.fs")
local config = require("rocks.config.internal")
local cache = require("rocks.cache")
local helpers = require("rocks.operations.helpers")
Expand All @@ -36,6 +35,7 @@ prune.prune = function(rock_name)
})
nio.run(function()
helpers.semaphore.with(function()
---@type MutRocksTomlRef
local user_config = helpers.parse_rocks_toml()
if user_config.plugins then
user_config.plugins[rock_name] = nil
Expand All @@ -56,7 +56,7 @@ prune.prune = function(rock_name)
progress_handle:report({ message = message, title = "Error" })
success = false
end
fs.write_file_await(config.config_path, "w", tostring(user_config))
user_config:write()
local user_rocks = config.get_user_rocks()
handlers.prune_user_rocks(user_rocks, report_progress, report_error)
adapter.synchronise_site_symlinks()
Expand Down
5 changes: 2 additions & 3 deletions lua/rocks/operations/unpin.lua
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,14 @@

local unpin = {}

local fs = require("rocks.fs")
local config = require("rocks.config.internal")
local helpers = require("rocks.operations.helpers")
local nio = require("nio")

---@param rock_name rock_name
unpin.unpin = function(rock_name)
nio.run(function()
helpers.semaphore.with(function()
---@type MutRocksTomlRef
local user_config = helpers.parse_rocks_toml()
local rocks_key, user_rock = helpers.get_rock_and_key(user_config, rock_name)
if not rocks_key or not user_rock then
Expand All @@ -41,7 +40,7 @@ unpin.unpin = function(rock_name)
else
user_config[rocks_key][rock_name].pin = nil
end
fs.write_file_await(config.config_path, "w", tostring(user_config))
user_config:write()
vim.schedule(function()
vim.notify(("%s unpinned"):format(rock_name), vim.log.levels.INFO)
end)
Expand Down
4 changes: 2 additions & 2 deletions lua/rocks/operations/update.lua
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ local update = {}

local constants = require("rocks.constants")
local log = require("rocks.log")
local fs = require("rocks.fs")
local config = require("rocks.config.internal")
local state = require("rocks.state")
local cache = require("rocks.cache")
Expand Down Expand Up @@ -85,6 +84,7 @@ update.update = function(on_complete, opts)
)
end

---@type MutRocksTomlRef
local user_rocks = helpers.parse_rocks_toml()

local to_update = vim.iter(state.outdated_rocks()):fold(
Expand Down Expand Up @@ -170,7 +170,7 @@ update.update = function(on_complete, opts)
user_rocks[rocks_key][rock_name] = installed_rock.version
end
end
fs.write_file_await(config.config_path, "w", tostring(user_rocks))
user_rocks:write()
nio.scheduler()
if not vim.tbl_isempty(error_handles) then
local message = "Update completed with errors! Run ':Rocks log' for details."
Expand Down

0 comments on commit 749d412

Please sign in to comment.