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 21, 2024
1 parent 9762134 commit e270a5d
Show file tree
Hide file tree
Showing 12 changed files with 219 additions and 32 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,24 @@ 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
1 change: 1 addition & 0 deletions doc/rocks.txt
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ RocksToml *RocksToml*
{plugins?} (table<rock_name,RockSpec>) The `[plugins]` entries
{servers?} (string[])
{dev_servers?} (string[])
{import?} (string[])
{string} (unknown) Fields that can be added by external modules


Expand Down
1 change: 1 addition & 0 deletions lua/rocks/api/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ end
---@field plugins? table<rock_name, RockSpec> The `[plugins]` entries
---@field servers? string[]
---@field dev_servers? string[]
---@field import? string[]
---@field [string] unknown Fields that can be added by external modules

---Returns a table with the parsed rocks.toml file.
Expand Down
85 changes: 70 additions & 15 deletions lua/rocks/config/internal.lua
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ end

local default_luarocks_binary = get_default_luarocks_binary(default_rocks_path)

local notified_recursive_imports = {}

--- rocks.nvim default configuration
---@class RocksConfig
local default_config = {
Expand Down Expand Up @@ -70,26 +72,79 @@ 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(parse_func: (fun(file_str: string, file_path: string): table, string[]), process_func: fun(config: table, file_path))
read_rocks_toml = function(parse_func, process_func)
local visited = {}

local function parse(file_path, default)
-- Don't allow recursive includes
if visited[file_path] then
if not notified_recursive_imports[file_path] then
vim.defer_fn(function()
vim.notify("Recursive import detected: " .. file_path, vim.log.levels.WARN)
end, 1000)
notified_recursive_imports[file_path] = true
end
return nil
end
visited[file_path] = true

-- Read config
local file_str = fs.read_or_create(file_path, default)
-- Parse and retrieve imports list
local rocks_toml, imports = parse_func(file_str, file_path)
-- Follow import paths (giving preference to imported config)
if imports then
for _, path in ipairs(imports) do
parse(fs.get_absolute_path(config.config_path, path), "")
end
end
-- Process result
process_func(rocks_toml, file_path)
end
parse(config.config_path, constants.DEFAULT_CONFIG)
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_merged = {}
config.read_rocks_toml(function(file_str, _)
-- Parse
local rocks_toml = require("toml_edit").parse_as_tbl(file_str)
local imports = rocks_toml.import
rocks_toml.import = nil
return rocks_toml, imports
end, function(rocks_toml, _)
-- Setup rockspec for rocks/plugins
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
end
end
end
end
end
return rocks_toml
-- Merge into configuration, in the order of preference returned by the read function
rocks_toml_merged = vim.tbl_deep_extend("keep", rocks_toml_merged, rocks_toml)
end)
rocks_toml_merged.import = nil -- Remove import field since we merged

return rocks_toml_merged
end,
---@return server_url[]
get_servers = function()
Expand Down
33 changes: 32 additions & 1 deletion lua/rocks/fs.lua
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,35 @@ 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
local home = os.getenv("HOME")
if home then
path = path:gsub("^~", home)
end
return path
end

--- Expand path string and get the absolute path if it a relative path string
---@param base_path string base path to use if path_str is relative
---@param path_str string the path string to expand
---@return string
function fs.get_absolute_path(base_path, path_str)
local path = fs.expand_path(path_str)
-- If path is not an absolute path, set it relative to the base
if path:sub(1, 1) ~= "/" then
path = vim.fs.joinpath(fs.expand_path(vim.fs.dirname(base_path)), path)
end
return path
end

--- Write `contents` to a file asynchronously
---@param location string file path
---@param mode string mode to open the file for
Expand Down Expand Up @@ -66,7 +95,9 @@ function fs.write_file(location, mode, contents, callback)
else
local msg = ("Error opening %s for writing: %s"):format(location, err)
log.error(msg)
vim.notify(msg, vim.log.levels.ERROR)
vim.schedule(function()
vim.notify(msg, vim.log.levels.ERROR)
end)
if callback then
callback()
end
Expand Down
6 changes: 2 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 @@ -98,7 +96,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 +215,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
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ local state = require("rocks.state")
local log = require("rocks.log")
local cache = require("rocks.cache")
local nio = require("nio")
local multi_mut_rocks_toml_wrapper = require("rocks.operations.helpers.multi_mut_rocks_toml_wrapper")

local helpers = {}

Expand All @@ -32,8 +33,22 @@ helpers.semaphore = nio.control.semaphore(1)
---Decode the user rocks from rocks.toml, creating a default config file if it does not exist
---@return MutRocksTomlRef
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_toml_configs = {}
config.read_rocks_toml(function(file_str, file_path)
-- Parse
local rocks_toml = require("toml_edit").parse(file_str)
-- TODO: Remove call to get_imports, this is needed because toml_edit.lua doesn't
-- seem to support toml Arrays
local imports = config.get_imports(file_path)
return rocks_toml, imports
end, function(rocks_toml, file_path)
---@type MutRocksTomlRefWithPath
local rocks_toml_config = { config = rocks_toml, path = file_path }
-- Append to config list in order of preference returned by the read function
table.insert(rocks_toml_configs, rocks_toml_config)
end)

return multi_mut_rocks_toml_wrapper.new(rocks_toml_configs) --[[@as MutRocksTomlRef]]
end

---@param rocks_toml MutRocksTomlRef
Expand Down
74 changes: 74 additions & 0 deletions lua/rocks/operations/helpers/multi_mut_rocks_toml_wrapper.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
local config = require("rocks.config.internal")
local fs = require("rocks.fs")

---@class MutRocksTomlRefWithPath
---@field config MutRocksTomlRef Config metatable
---@field path? string The path to the configuration

---@class MultiMutRocksTomlWrapper
---@field cache table<string, MultiMutRocksTomlWrapper> Cache for nested metatables
---@field configs MutRocksTomlRefWithPath[] A list of 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 nested_tables = {}
for _, tbl in ipairs(self.configs) do
if tbl.config[key] ~= nil then
if type(tbl.config[key]) == "table" then
table.insert(nested_tables, { config = tbl.config[key], path = tbl.path })
else
return tbl.config[key]
end
end
end
-- If the value is a table, setup a nested metatable that uses the
-- inner tables of the config tables
if #nested_tables > 0 then
if not self.cache[key] then
self.cache[key] = MultiMutRocksTomlWrapper.new(nested_tables)
end
return self.cache[key]
end
return nil
end
MultiMutRocksTomlWrapper.__newindex = function(self, key, value)
local insert_index = 1
for i, tbl in ipairs(self.configs) do
-- Insert into base config by default
if tbl.path == config.config_path then
insert_index = i
end
if tbl.config[key] ~= nil then
tbl.config[key] = value
return
end
end
-- If key not found in any table, add it to the first table
self.configs[insert_index].config[key] = value
end

--- Write to all rocks toml config files in an async context
---@type async fun(self: MultiMutRocksTomlWrapper)
function MultiMutRocksTomlWrapper:write()
for _, tbl in ipairs(self.configs) do
if tbl.path ~= nil then
fs.write_file_await(tbl.path, "w", tostring(tbl.config))
end
end
end

--- Function to create a new wrapper
---@param configs MutRocksTomlRefWithPath[] A list of rocks toml configs
---@return MultiMutRocksTomlWrapper
function MultiMutRocksTomlWrapper.new(configs)
assert(#configs > 0, "Must provide atleast one rocks toml config")
local self = { cache = {}, configs = configs }
setmetatable(self, MultiMutRocksTomlWrapper)
return self
end

return MultiMutRocksTomlWrapper
4 changes: 1 addition & 3 deletions lua/rocks/operations/pin.lua
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@

local pin = {}

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

Expand All @@ -40,7 +38,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
3 changes: 1 addition & 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 Down Expand Up @@ -56,7 +55,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
4 changes: 1 addition & 3 deletions lua/rocks/operations/unpin.lua
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@

local unpin = {}

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

Expand All @@ -41,7 +39,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
3 changes: 1 addition & 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 @@ -170,7 +169,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 e270a5d

Please sign in to comment.