From c847330e797eb08e6fe5795ac612d2f282d42b73 Mon Sep 17 00:00:00 2001 From: Simi Falaye Date: Sat, 19 Oct 2024 00:21:41 -0600 Subject: [PATCH] feat: Add support for config imports --- README.md | 17 +++++ lua/rocks/config/internal.lua | 69 ++++++++++++++++---- lua/rocks/fs.lua | 27 +++++++- lua/rocks/operations/add.lua | 9 ++- lua/rocks/operations/helpers.lua | 107 ++++++++++++++++++++++++++++++- lua/rocks/operations/pin.lua | 7 +- lua/rocks/operations/prune.lua | 6 +- lua/rocks/operations/unpin.lua | 7 +- lua/rocks/operations/update.lua | 6 +- 9 files changed, 219 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 82c4077..6db44a9 100644 --- a/README.md +++ b/README.md @@ -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`. diff --git a/lua/rocks/config/internal.lua b/lua/rocks/config/internal.lua index decf619..71beb72 100644 --- a/lua/rocks/config/internal.lua +++ b/lua/rocks/config/internal.lua @@ -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[] diff --git a/lua/rocks/fs.lua b/lua/rocks/fs.lua index 4f99127..d5ddee6 100644 --- a/lua/rocks/fs.lua +++ b/lua/rocks/fs.lua @@ -36,6 +36,29 @@ 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 + +--- 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 @@ -66,7 +89,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 diff --git a/lua/rocks/operations/add.lua b/lua/rocks/operations/add.lua index bb5ec86..2888e13 100644 --- a/lua/rocks/operations/add.lua +++ b/lua/rocks/operations/add.lua @@ -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") @@ -89,7 +87,8 @@ add.add = function(arg_list, callback, opts) nio.run(function() helpers.semaphore.with(function() - local user_rocks = helpers.parse_rocks_toml() + ---@type MutRocksTomlRef + local user_rocks = helpers.parse_rocks_toml() --[[@as MutRocksTomlRef]] local handler = handlers.get_install_handler_callback(user_rocks, arg_list) if type(handler) == "function" then local function report_progress(message) @@ -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 @@ -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() diff --git a/lua/rocks/operations/helpers.lua b/lua/rocks/operations/helpers.lua index f98e2c2..32efa3e 100644 --- a/lua/rocks/operations/helpers.lua +++ b/lua/rocks/operations/helpers.lua @@ -29,11 +29,112 @@ local helpers = {} helpers.semaphore = nio.control.semaphore(1) +---@class MultiMutRocksTomlWrapper +---@field cache table 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 + +--- Write to all rocks toml config files +function MultiMutRocksTomlWrapper:write() + 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 + +--- 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 + ---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 diff --git a/lua/rocks/operations/pin.lua b/lua/rocks/operations/pin.lua index a69835b..68771b3 100644 --- a/lua/rocks/operations/pin.lua +++ b/lua/rocks/operations/pin.lua @@ -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") @@ -25,7 +23,8 @@ local nio = require("nio") pin.pin = function(rock_name) nio.run(function() helpers.semaphore.with(function() - local user_config = helpers.parse_rocks_toml() + ---@type MutRocksTomlRef + local user_config = helpers.parse_rocks_toml() --[[@as MutRocksTomlRef]] local rocks_key, user_rock = helpers.get_rock_and_key(user_config, rock_name) if not rocks_key then vim.schedule(function() @@ -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) diff --git a/lua/rocks/operations/prune.lua b/lua/rocks/operations/prune.lua index c690580..d9301e3 100644 --- a/lua/rocks/operations/prune.lua +++ b/lua/rocks/operations/prune.lua @@ -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") @@ -36,7 +35,8 @@ prune.prune = function(rock_name) }) nio.run(function() helpers.semaphore.with(function() - local user_config = helpers.parse_rocks_toml() + ---@type MutRocksTomlRef + local user_config = helpers.parse_rocks_toml() --[[@as MutRocksTomlRef]] if user_config.plugins then user_config.plugins[rock_name] = nil end @@ -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() diff --git a/lua/rocks/operations/unpin.lua b/lua/rocks/operations/unpin.lua index 3a16fac..4753be9 100644 --- a/lua/rocks/operations/unpin.lua +++ b/lua/rocks/operations/unpin.lua @@ -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") @@ -25,7 +23,8 @@ local nio = require("nio") unpin.unpin = function(rock_name) nio.run(function() helpers.semaphore.with(function() - local user_config = helpers.parse_rocks_toml() + ---@type MutRocksTomlRef + local user_config = helpers.parse_rocks_toml() --[[@as MutRocksTomlRef]] local rocks_key, user_rock = helpers.get_rock_and_key(user_config, rock_name) if not rocks_key or not user_rock then vim.schedule(function() @@ -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) diff --git a/lua/rocks/operations/update.lua b/lua/rocks/operations/update.lua index 21fea8b..a8df8b5 100644 --- a/lua/rocks/operations/update.lua +++ b/lua/rocks/operations/update.lua @@ -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") @@ -85,7 +84,8 @@ update.update = function(on_complete, opts) ) end - local user_rocks = helpers.parse_rocks_toml() + ---@type MutRocksTomlRef + local user_rocks = helpers.parse_rocks_toml() --[[@as MutRocksTomlRef]] local to_update = vim.iter(state.outdated_rocks()):fold( {}, @@ -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."