From 6dc1603ea408f476a57937bbeaf7f86520a21a98 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Tue, 7 May 2024 16:25:03 -0700 Subject: feat: formatters can use $RELATIVE_FILEPATH in args (#349) --- README.md | 4 +-- doc/conform.txt | 4 +-- lua/conform/fs.lua | 68 +++++++++++++++++++++++++++++++++++++++++++++++++ lua/conform/runner.lua | 13 +++++++++- scripts/options_doc.lua | 4 +-- tests/fs_spec.lua | 22 ++++++++++++++++ 6 files changed, 108 insertions(+), 7 deletions(-) create mode 100644 tests/fs_spec.lua diff --git a/README.md b/README.md index 027a7ce..2ff572b 100644 --- a/README.md +++ b/README.md @@ -464,7 +464,7 @@ require("conform").setup({ -- Return a single string instead of a list to run the command in a shell args = { "--stdin-from-filename", "$FILENAME" }, -- If the formatter supports range formatting, create the range arguments here - range_args = function(ctx) + range_args = function(self, ctx) return { "--line-start", ctx.range.start[1], "--line-end", ctx.range["end"][1] } end, -- Send file contents to stdin, read new contents from stdout (default true) @@ -478,7 +478,7 @@ require("conform").setup({ -- When stdin=false, use this template to generate the temporary file that gets formatted tmpfile_format = ".conform.$RANDOM.$FILENAME", -- When returns false, the formatter will not be used - condition = function(ctx) + condition = function(self, ctx) return vim.fs.basename(ctx.filename) ~= "README.md" end, -- Exit codes that indicate success (default { 0 }) diff --git a/doc/conform.txt b/doc/conform.txt index a7aed2a..977a0f0 100644 --- a/doc/conform.txt +++ b/doc/conform.txt @@ -61,7 +61,7 @@ OPTIONS *conform-option -- Return a single string instead of a list to run the command in a shell args = { "--stdin-from-filename", "$FILENAME" }, -- If the formatter supports range formatting, create the range arguments here - range_args = function(ctx) + range_args = function(self, ctx) return { "--line-start", ctx.range.start[1], "--line-end", ctx.range["end"][1] } end, -- Send file contents to stdin, read new contents from stdout (default true) @@ -75,7 +75,7 @@ OPTIONS *conform-option -- When stdin=false, use this template to generate the temporary file that gets formatted tmpfile_format = ".conform.$RANDOM.$FILENAME", -- When returns false, the formatter will not be used - condition = function(ctx) + condition = function(self, ctx) return vim.fs.basename(ctx.filename) ~= "README.md" end, -- Exit codes that indicate success (default { 0 }) diff --git a/lua/conform/fs.lua b/lua/conform/fs.lua index d303dbd..6f92e18 100644 --- a/lua/conform/fs.lua +++ b/lua/conform/fs.lua @@ -15,4 +15,72 @@ M.join = function(...) return table.concat({ ... }, M.sep) end +M.is_absolute = function(path) + if M.is_windows then + return path:lower():match("^%a:") + else + return vim.startswith(path, "/") + end +end + +M.abspath = function(path) + if not M.is_absolute(path) then + path = vim.fn.fnamemodify(path, ":p") + end + return path +end + +--- Returns true if candidate is a subpath of root, or if they are the same path. +---@param root string +---@param candidate string +---@return boolean +M.is_subpath = function(root, candidate) + if candidate == "" then + return false + end + root = vim.fs.normalize(M.abspath(root)) + -- Trim trailing "/" from the root + if root:find("/", -1) then + root = root:sub(1, -2) + end + candidate = vim.fs.normalize(M.abspath(candidate)) + if M.is_windows then + root = root:lower() + candidate = candidate:lower() + end + if root == candidate then + return true + end + local prefix = candidate:sub(1, root:len()) + if prefix ~= root then + return false + end + + local candidate_starts_with_sep = candidate:find("/", root:len() + 1, true) == root:len() + 1 + local root_ends_with_sep = root:find("/", root:len(), true) == root:len() + + return candidate_starts_with_sep or root_ends_with_sep +end + +---Create a relative path from the source to the target +---@param source string +---@param target string +---@return string +M.relative_path = function(source, target) + source = M.abspath(source) + target = M.abspath(target) + local path = {} + while not M.is_subpath(source, target) do + table.insert(path, "..") + local new_source = vim.fs.dirname(source) + assert(source ~= new_source) + source = new_source + end + + local offset = vim.endswith(source, M.sep) and 1 or 2 + local rel_target = target:sub(source:len() + offset) + table.insert(path, rel_target) + return M.join(unpack(path)) +end + return M diff --git a/lua/conform/runner.lua b/lua/conform/runner.lua index 0f31e64..b3a1e61 100644 --- a/lua/conform/runner.lua +++ b/lua/conform/runner.lua @@ -33,8 +33,17 @@ M.build_cmd = function(formatter_name, ctx, config) end end + local cwd + if config.cwd then + cwd = config.cwd(config, ctx) + end + local relative_filename = fs.relative_path(cwd or vim.fn.getcwd(), ctx.filename) + if type(args) == "string" then - local interpolated = args:gsub("$FILENAME", ctx.filename):gsub("$DIRNAME", ctx.dirname) + local interpolated = args + :gsub("$FILENAME", ctx.filename) + :gsub("$DIRNAME", ctx.dirname) + :gsub("$RELATIVE_FILEPATH", relative_filename) return command .. " " .. interpolated else local cmd = { command } @@ -44,6 +53,8 @@ M.build_cmd = function(formatter_name, ctx, config) v = ctx.filename elseif v == "$DIRNAME" then v = ctx.dirname + elseif v == "$RELATIVE_FILEPATH" then + v = relative_filename end table.insert(cmd, v) end diff --git a/scripts/options_doc.lua b/scripts/options_doc.lua index 96c0f2b..3e22802 100644 --- a/scripts/options_doc.lua +++ b/scripts/options_doc.lua @@ -48,7 +48,7 @@ require("conform").setup({ -- Return a single string instead of a list to run the command in a shell args = { "--stdin-from-filename", "$FILENAME" }, -- If the formatter supports range formatting, create the range arguments here - range_args = function(ctx) + range_args = function(self, ctx) return { "--line-start", ctx.range.start[1], "--line-end", ctx.range["end"][1] } end, -- Send file contents to stdin, read new contents from stdout (default true) @@ -62,7 +62,7 @@ require("conform").setup({ -- When stdin=false, use this template to generate the temporary file that gets formatted tmpfile_format = ".conform.$RANDOM.$FILENAME", -- When returns false, the formatter will not be used - condition = function(ctx) + condition = function(self, ctx) return vim.fs.basename(ctx.filename) ~= "README.md" end, -- Exit codes that indicate success (default { 0 }) diff --git a/tests/fs_spec.lua b/tests/fs_spec.lua new file mode 100644 index 0000000..2d788c9 --- /dev/null +++ b/tests/fs_spec.lua @@ -0,0 +1,22 @@ +local fs = require("conform.fs") + +describe("fs", function() + local relative_paths = { + { "/home", "/home/file.txt", "file.txt" }, + { "/home/", "/home/file.txt", "file.txt" }, + { "/home", "/foo/file.txt", "../foo/file.txt" }, + { "/home/foo", "/home/bar/file.txt", "../bar/file.txt" }, + { "/home", "/file.txt", "../file.txt" }, + { "/home", "/home/foo/file.txt", "foo/file.txt" }, + { ".", "foo/file.txt", "foo/file.txt" }, + { "home", "home/file.txt", "file.txt" }, + { "home", "file.txt", "../file.txt" }, + } + + it("relative_path", function() + for _, paths in ipairs(relative_paths) do + local source, target, expected = unpack(paths) + assert.are.same(fs.relative_path(source, target), expected) + end + end) +end) -- cgit v1.2.3-70-g09d2