diff --git a/lua/competitest/config.lua b/lua/competitest/config.lua index b8755fb..aec8e74 100644 --- a/lua/competitest/config.lua +++ b/lua/competitest/config.lua @@ -279,6 +279,8 @@ function M.update_config_table(cfg_tbl, opts) new_config.run_command[lang].args = cmd.args end end + -- Normalize path-related options for global setup configuration + new_config = M.normalize_path_config(new_config) return new_config end @@ -290,6 +292,43 @@ M.current_setup = nil ---@type table M.buffer_configs = {} +---Normalize path-related configuration options +---Expands ~ to home directory in path configuration options +---@param cfg competitest.Config configuration to normalize +---@return competitest.Config # configuration with normalized paths +function M.normalize_path_config(cfg) + local utils = require("competitest.utils") + + -- These options can contain absolute or ~ paths that should be expanded + -- They are NOT relative to current file, so we normalize them during config loading + local absolute_path_options = { + "received_problems_path", + "received_contests_directory", + "received_contests_problems_path", + } + + for _, opt in ipairs(absolute_path_options) do + if cfg[opt] and type(cfg[opt]) == "string" then + cfg[opt] = utils.normalize_path(cfg[opt]) + end + end + + -- Handle template_file which can be false, string, or table + if cfg.template_file then + if type(cfg.template_file) == "string" then + cfg.template_file = utils.normalize_path(cfg.template_file) + elseif type(cfg.template_file) == "table" then + for ext, path in pairs(cfg.template_file) do + if type(path) == "string" then + cfg.template_file[ext] = utils.normalize_path(path) + end + end + end + end + + return cfg +end + ---Load local configuration for given directory ---@param directory string ---@return competitest.Config? # local configuration, or `nil` when it's absent or incorrect @@ -305,6 +344,8 @@ function M.load_local_config(directory) utils.notify("load_buffer_config: '" .. config_file .. "' doesn't return a table.") return nil end + -- Normalize path-related configuration options + local_config = M.normalize_path_config(local_config) return local_config end directory = vim.fn.fnamemodify(directory, ":h") diff --git a/lua/competitest/receive.lua b/lua/competitest/receive.lua index 5662d16..ef4ba48 100644 --- a/lua/competitest/receive.lua +++ b/lua/competitest/receive.lua @@ -462,7 +462,7 @@ function storage_utils.store_received_task_config(filepath, confirm_overwriting, end local testcases = require("competitest.testcases") - local tcdir = file_directory .. "/" .. cfg.testcases_directory .. "/" + local tcdir = utils.resolve_config_path(cfg.testcases_directory, file_directory) .. "/" if cfg.testcases_use_single_file then local single_file_path = tcdir .. utils.eval_string(filepath, cfg.testcases_single_file_format) testcases.single_file.write(single_file_path, tctbl) diff --git a/lua/competitest/runner.lua b/lua/competitest/runner.lua index df999ea..fc0f0ab 100644 --- a/lua/competitest/runner.lua +++ b/lua/competitest/runner.lua @@ -103,8 +103,8 @@ function TCRunner:new(bufnr) bufnr = bufnr, cc = compile_command, rc = run_command, - compile_directory = filedir .. buf_cfg.compile_directory .. "/", - running_directory = filedir .. buf_cfg.running_directory .. "/", + compile_directory = utils.resolve_config_path(buf_cfg.compile_directory, filedir) .. "/", + running_directory = utils.resolve_config_path(buf_cfg.running_directory, filedir) .. "/", tcdata = {}, compile = compile_command ~= nil, next_tc = 1, diff --git a/lua/competitest/testcases.lua b/lua/competitest/testcases.lua index 26cb506..be48c91 100644 --- a/lua/competitest/testcases.lua +++ b/lua/competitest/testcases.lua @@ -207,7 +207,8 @@ end ---@return string # absolute path of testcases directory local function buf_get_testcases_path(bufnr) return vim.api.nvim_buf_call(bufnr, function() - return vim.fn.expand("%:p:h") .. "/" .. gbc(bufnr).testcases_directory .. "/" + local base_dir = vim.fn.expand("%:p:h") + return utils.resolve_config_path(gbc(bufnr).testcases_directory, base_dir) .. "/" end) end diff --git a/lua/competitest/utils.lua b/lua/competitest/utils.lua index 644f09e..13c84eb 100644 --- a/lua/competitest/utils.lua +++ b/lua/competitest/utils.lua @@ -113,6 +113,45 @@ function M.buf_eval_string(bufnr, str, tcnum) return M.eval_string(vim.api.nvim_buf_get_name(bufnr), str) end +---Expand and normalize a file path +---Expands `~` to home directory and converts to absolute path +---@param path string file path to expand and normalize +---@return string # expanded and normalized absolute path +function M.normalize_path(path) + -- Expand ~ to home directory + local expanded_path = path + if string.sub(path, 1, 1) == "~" then + expanded_path = luv.os_homedir() .. string.sub(path, 2) + end + + -- Convert to absolute path + local absolute_path = vim.fn.fnamemodify(expanded_path, ":p") + + -- Normalize path (resolve . and .., remove redundant separators) + local normalized = luv.fs_realpath(absolute_path) + return normalized or absolute_path +end + +---Resolve a path configuration option that can be relative or absolute +---If path starts with ~ or /, treat it as absolute and expand ~ +---Otherwise, return it as-is to be treated as relative to base directory +---@param path string path from configuration +---@param base_dir string base directory for relative paths +---@return string # resolved path +function M.resolve_config_path(path, base_dir) + if not path or path == "" then + return base_dir + end + + -- If path starts with ~ or /, it's absolute + if string.sub(path, 1, 1) == "~" or string.sub(path, 1, 1) == "/" then + return M.normalize_path(path) + end + + -- Otherwise, it's relative to base_dir + return base_dir .. "/" .. path +end + ---Returns `true` if the given file exists, `false` otherwise ---@param filepath string ---@return boolean