diff --git a/README.md b/README.md index 82c4077..1a52184 100644 --- a/README.md +++ b/README.md @@ -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`. diff --git a/doc/rocks.txt b/doc/rocks.txt index 9d5c226..a17d215 100644 --- a/doc/rocks.txt +++ b/doc/rocks.txt @@ -330,6 +330,7 @@ RocksToml *RocksToml* {plugins?} (table) The `[plugins]` entries {servers?} (string[]) {dev_servers?} (string[]) + {import?} (string[]) {string} (unknown) Fields that can be added by external modules diff --git a/lua/rocks/api/init.lua b/lua/rocks/api/init.lua index fa73d97..a478347 100644 --- a/lua/rocks/api/init.lua +++ b/lua/rocks/api/init.lua @@ -117,6 +117,7 @@ end ---@field plugins? table 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. diff --git a/lua/rocks/config/internal.lua b/lua/rocks/config/internal.lua index decf619..80bf55d 100644 --- a/lua/rocks/config/internal.lua +++ b/lua/rocks/config/internal.lua @@ -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 = { @@ -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() diff --git a/lua/rocks/fs.lua b/lua/rocks/fs.lua index 4f99127..e288bf5 100644 --- a/lua/rocks/fs.lua +++ b/lua/rocks/fs.lua @@ -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 @@ -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 diff --git a/lua/rocks/operations/add.lua b/lua/rocks/operations/add.lua index bb5ec86..47206bc 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") @@ -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 @@ -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() diff --git a/lua/rocks/operations/helpers.lua b/lua/rocks/operations/helpers/init.lua similarity index 93% rename from lua/rocks/operations/helpers.lua rename to lua/rocks/operations/helpers/init.lua index f98e2c2..7b3fe52 100644 --- a/lua/rocks/operations/helpers.lua +++ b/lua/rocks/operations/helpers/init.lua @@ -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 = {} @@ -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 diff --git a/lua/rocks/operations/helpers/multi_mut_rocks_toml_wrapper.lua b/lua/rocks/operations/helpers/multi_mut_rocks_toml_wrapper.lua new file mode 100644 index 0000000..d8aaa07 --- /dev/null +++ b/lua/rocks/operations/helpers/multi_mut_rocks_toml_wrapper.lua @@ -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 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 diff --git a/lua/rocks/operations/pin.lua b/lua/rocks/operations/pin.lua index a69835b..eae513f 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") @@ -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) diff --git a/lua/rocks/operations/prune.lua b/lua/rocks/operations/prune.lua index c690580..4a42f08 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") @@ -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() diff --git a/lua/rocks/operations/unpin.lua b/lua/rocks/operations/unpin.lua index 3a16fac..27217fe 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") @@ -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) diff --git a/lua/rocks/operations/update.lua b/lua/rocks/operations/update.lua index 21fea8b..e762166 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") @@ -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."