Closed nickpoorman closed 2 weeks ago
The most common way to define a formatter is to have a cmd
and args
, which will start a new process and do the formatting by piping stdin/stdout, but you can also define a formatter as an async lua function. The only built-in formatter currently doing this is the injected
formatter (to format injected languages, using treesitter). You can see the function signature and how it is used here:
https://github.com/stevearc/conform.nvim/blob/6e5d476e97dbd251cc2233d42fd238c810404701/lua/conform/formatters/injected.lua#L145
Wow! Thanks for the quick response! That was actually super easy and it worked beautifully!
-- ~/.config/nvim/lua/plugins/conform.lua
local ruby_comment_formatter = require("custom.ruby_comment_formatter")
return {
"stevearc/conform.nvim",
opts = {
-- LazyVim will use these options when formatting with the conform.nvim formatter
format = {
timeout_ms = 3000,
async = false, -- not recommended to change
quiet = false, -- not recommended to change
lsp_fallback = true, -- not recommended to change
},
formatters_by_ft = {
ruby = { "ruby_comment_formatter", "rubocop" },
},
formatters = {
ruby_comment_formatter = {
format = function(_, ctx, lines, callback)
ruby_comment_formatter(_, ctx, lines, callback)
end,
},
},
},
}
The updated formatter:
-- ~/.config/nvim/lua/customruby_comment_formatter.lua
local function trim(s)
return s:match("^%s*(.-)%s*$")
end
local function is_comment_line(line, comment_prefix)
return line:find("^%s*" .. comment_prefix)
end
local function determine_indentation(line)
return #line:match("^%s*")
end
local function split_comment_block(comment_prefix, lines)
local blocks = {}
local block = {}
for _, line in ipairs(lines) do
if trim(line) == comment_prefix then
if #block > 0 then
table.insert(blocks, block)
block = {}
end
else
table.insert(block, line)
end
end
if #block > 0 then
table.insert(blocks, block)
end
return blocks
end
local function join_sub_blocks_with_empty_comment_lines(comment_prefix, indentation, sub_blocks)
local joined_blocks = {}
for i, block in ipairs(sub_blocks) do
for _, line in ipairs(block) do
table.insert(joined_blocks, line)
end
if i < #sub_blocks then
table.insert(joined_blocks, string.rep(" ", indentation) .. comment_prefix)
end
end
return joined_blocks
end
local function format_block(comment_prefix, indentation, max_line_length, block)
local block_single_line = ""
for _, line in ipairs(block) do
local trimmed_line = trim(line)
block_single_line = block_single_line .. " " .. trimmed_line:gsub("^%s*" .. comment_prefix .. "%s*", "")
end
local words = {}
for word in block_single_line:gmatch("%S+") do
table.insert(words, word)
end
local formatted_lines = {}
local line = string.rep(" ", indentation) .. comment_prefix
for _, word in ipairs(words) do
if (#line + #word + 1) > max_line_length then
table.insert(formatted_lines, line)
line = string.rep(" ", indentation) .. comment_prefix .. " " .. word
else
line = line .. " " .. word
end
end
table.insert(formatted_lines, line)
return formatted_lines
end
local function format_blocks(comment_prefix, indentation, max_line_length, blocks)
local formatted_blocks = {}
for _, block in ipairs(blocks) do
table.insert(formatted_blocks, format_block(comment_prefix, indentation, max_line_length, block))
end
return formatted_blocks
end
local function replace_lines(lines, start, end_, replace_with)
local head_lines = { table.unpack(lines, 1, start) }
local tail_lines = { table.unpack(lines, end_ + 2) }
local output = {}
for _, line in ipairs(head_lines) do
table.insert(output, line)
end
for _, line in ipairs(replace_with) do
table.insert(output, line)
end
for _, line in ipairs(tail_lines) do
table.insert(output, line)
end
return output
end
local function find_comment_block(comment_prefix, lines, line_number)
local start, end_ = line_number, line_number
while start > 1 and is_comment_line(lines[start - 1], comment_prefix) do
start = start - 1
end
while end_ < #lines and is_comment_line(lines[end_ + 1], comment_prefix) do
end_ = end_ + 1
end
return start, end_
end
local function format_comment_block(comment_prefix, lines, line_number, max_line_length)
local indentation = determine_indentation(lines[line_number])
local start, end_ = find_comment_block(comment_prefix, lines, line_number)
local comment_block = { table.unpack(lines, start, end_) }
local blocks = split_comment_block(comment_prefix, comment_block)
local formatted_blocks = format_blocks(comment_prefix, indentation, max_line_length, blocks)
local joined_blocks = join_sub_blocks_with_empty_comment_lines(comment_prefix, indentation, formatted_blocks)
return replace_lines(lines, start - 1, end_ - 1, joined_blocks)
end
return function(_, ctx, lines, callback)
local comment_prefix = "#"
local max_line_length = vim.api.nvim_get_option_value("textwidth", { buf = ctx.bufnr })
if max_line_length == 0 then
max_line_length = 80
end
local i = 1
while i <= #lines do
if is_comment_line(lines[i], comment_prefix) then
lines = format_comment_block(comment_prefix, lines, i, max_line_length)
-- Update the line index to continue processing after the current block
local _, end_ = find_comment_block(comment_prefix, lines, i)
i = end_ + 1
else
i = i + 1
end
end
callback(nil, lines)
end
Did you check existing requests?
Describe the feature
I did some searching and I'm sure if this is possible yet with conform... I'd like to require a lua file (once when nvim loads) and then execute a function that simply formats the current buffer.
Provide background
I have the following formatter that I wrote in lua that I'd like to run on ruby files (to format the comments before handing off to the rubocop formatter.
Something like this:
I'd like to load my formatter once when nvim loads conform, and then simply have it execute a format function on the current buffer. But it looks like the only way to do that right now is to have my formatter in it's own
.lua
file and then executelua my_formatter.lua
as a custom formatter each time. Seems kinda silly when this is all native lua that could be loaded and executed. Is there a way to do this with conform?My formatter for reference:
What is the significance of this feature?
nice to have
Additional details
No response