diff options
author | Steven Arcangeli <stevearc@stevearc.com> | 2023-08-28 18:28:07 -0700 |
---|---|---|
committer | Steven Arcangeli <stevearc@stevearc.com> | 2023-08-28 18:28:07 -0700 |
commit | cddd536e087a9fd3d2c9ea5b0a44e46c7b4b54c2 (patch) | |
tree | 70f6868440596ae90b7f451379c3abfa5678849c | |
parent | 69c4495ab5ad3c07c3a4f3c2bcac2f070718b4cb (diff) |
feat: range formatting
Should work the same as vim.lsp.buf.format(). Additionally, range
formatting is supported for *any* formatter. If the formatter doesn't
have native support for ranges, conform will do its best to only apply the
diffs that affect that range.
46 files changed, 236 insertions, 82 deletions
@@ -232,6 +232,10 @@ require("conform").setup({ -- OPTIONAL - all fields below this are optional -- A list of strings, or a function that returns a list of strings args = { "--stdin-from-filename", "$FILENAME" }, + -- If the formatter supports range formatting, create the range arguments here + range_args = function(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) -- When false, will create a temp file (will appear in "$FILENAME" args). The temp -- file is assumed to be modified in-place by the format command. @@ -306,15 +310,16 @@ vim.api.nvim_create_autocmd("BufWritePre", { `format(opts): boolean` \ Format a buffer -| Param | Type | Desc | | -| ----- | ------------ | --------------- | ------------------------------------------------------------------------------------------ | -| opts | `nil\|table` | | | -| | timeout_ms | `nil\|integer` | Time in milliseconds to block for formatting. Defaults to 1000. No effect if async = true. | -| | bufnr | `nil\|integer` | Format this buffer (default 0) | -| | async | `nil\|boolean` | If true the method won't block. Defaults to false. | -| | formatters | `nil\|string[]` | List of formatters to run. Defaults to all formatters for the buffer filetype. | -| | lsp_fallback | `nil\|boolean` | Attempt LSP formatting if no formatters are available. Defaults to false. | -| | quiet | `nil\|boolean` | Don't show any notifications for warnings or failures. Defaults to false. | +| Param | Type | Desc | | +| ----- | ------------ | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| opts | `nil\|table` | | | +| | timeout_ms | `nil\|integer` | Time in milliseconds to block for formatting. Defaults to 1000. No effect if async = true. | +| | bufnr | `nil\|integer` | Format this buffer (default 0) | +| | async | `nil\|boolean` | If true the method won't block. Defaults to false. | +| | formatters | `nil\|string[]` | List of formatters to run. Defaults to all formatters for the buffer filetype. | +| | lsp_fallback | `nil\|boolean` | Attempt LSP formatting if no formatters are available. Defaults to false. | +| | quiet | `nil\|boolean` | Don't show any notifications for warnings or failures. Defaults to false. | +| | range | `nil\|table` | Range to format. Table must contain `start` and `end` keys with {row, col} tuples using (1,0) indexing. Defaults to current selection in visual mode | Returns: diff --git a/doc/conform.txt b/doc/conform.txt index f602580..f0b97a0 100644 --- a/doc/conform.txt +++ b/doc/conform.txt @@ -44,6 +44,10 @@ OPTIONS *conform-option -- OPTIONAL - all fields below this are optional -- A list of strings, or a function that returns a list of strings args = { "--stdin-from-filename", "$FILENAME" }, + -- If the formatter supports range formatting, create the range arguments here + range_args = function(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) -- When false, will create a temp file (will appear in "$FILENAME" args). The temp -- file is assumed to be modified in-place by the format command. @@ -95,6 +99,9 @@ format({opts}): boolean *conform.forma are available. Defaults to false. {quiet} `nil|boolean` Don't show any notifications for warnings or failures. Defaults to false. + {range} `nil|table` Range to format. Table must contain `start` + and `end` keys with {row, col} tuples using (1,0) + indexing. Defaults to current selection in visual mode Returns: `boolean` True if any formatters were attempted diff --git a/lua/conform/formatters/autoflake.lua b/lua/conform/formatters/autoflake.lua index 04daf71..c7d0a3b 100644 --- a/lua/conform/formatters/autoflake.lua +++ b/lua/conform/formatters/autoflake.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/PyCQA/autoflake", diff --git a/lua/conform/formatters/autopep8.lua b/lua/conform/formatters/autopep8.lua index 5ed2f83..3945e47 100644 --- a/lua/conform/formatters/autopep8.lua +++ b/lua/conform/formatters/autopep8.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/hhatto/autopep8", @@ -6,4 +6,7 @@ return { }, command = "autopep8", args = { "-" }, + range_args = function(ctx) + return { "-", "--line-range", tostring(ctx.range.start[1]), tostring(ctx.range["end"][1]) } + end, } diff --git a/lua/conform/formatters/black.lua b/lua/conform/formatters/black.lua index 0d892a2..e27ed90 100644 --- a/lua/conform/formatters/black.lua +++ b/lua/conform/formatters/black.lua @@ -1,5 +1,5 @@ local util = require("conform.util") ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/psf/black", diff --git a/lua/conform/formatters/clang_format.lua b/lua/conform/formatters/clang_format.lua index d2c6d41..44af877 100644 --- a/lua/conform/formatters/clang_format.lua +++ b/lua/conform/formatters/clang_format.lua @@ -1,4 +1,5 @@ ----@type conform.FormatterConfig +local util = require("conform.util") +---@type conform.FileFormatterConfig return { meta = { url = "https://www.kernel.org/doc/html/latest/process/clang-format.html", @@ -6,4 +7,16 @@ return { }, command = "clang-format", args = { "-assume-filename", "$FILENAME" }, + range_args = function(ctx) + local start_offset, end_offset = util.get_offsets_from_range(ctx.buf, ctx.range) + local length = end_offset - start_offset + return { + "-assume-filename", + "$FILENAME", + "--offset", + tostring(start_offset), + "--length", + tostring(length), + } + end, } diff --git a/lua/conform/formatters/cljstyle.lua b/lua/conform/formatters/cljstyle.lua index ffd8061..21205a5 100644 --- a/lua/conform/formatters/cljstyle.lua +++ b/lua/conform/formatters/cljstyle.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/greglook/cljstyle", diff --git a/lua/conform/formatters/cmake_format.lua b/lua/conform/formatters/cmake_format.lua index 563bcae..0e98108 100644 --- a/lua/conform/formatters/cmake_format.lua +++ b/lua/conform/formatters/cmake_format.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/cheshirekow/cmake_format", diff --git a/lua/conform/formatters/dart_format.lua b/lua/conform/formatters/dart_format.lua index eb9b39c..e110b06 100644 --- a/lua/conform/formatters/dart_format.lua +++ b/lua/conform/formatters/dart_format.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://dart.dev/tools/dart-format", diff --git a/lua/conform/formatters/dfmt.lua b/lua/conform/formatters/dfmt.lua index 49c99cb..41dd667 100644 --- a/lua/conform/formatters/dfmt.lua +++ b/lua/conform/formatters/dfmt.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/dlang-community/dfmt", diff --git a/lua/conform/formatters/elm_format.lua b/lua/conform/formatters/elm_format.lua index 23f1408..5b0db5c 100644 --- a/lua/conform/formatters/elm_format.lua +++ b/lua/conform/formatters/elm_format.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/avh4/elm-format", diff --git a/lua/conform/formatters/erb_format.lua b/lua/conform/formatters/erb_format.lua index dad08ca..65d7d10 100644 --- a/lua/conform/formatters/erb_format.lua +++ b/lua/conform/formatters/erb_format.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/nebulab/erb-formatter", diff --git a/lua/conform/formatters/eslint_d.lua b/lua/conform/formatters/eslint_d.lua index e036aae..e7a5227 100644 --- a/lua/conform/formatters/eslint_d.lua +++ b/lua/conform/formatters/eslint_d.lua @@ -1,5 +1,5 @@ local util = require("conform.util") ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/mantoni/eslint_d.js/", diff --git a/lua/conform/formatters/fish_indent.lua b/lua/conform/formatters/fish_indent.lua index 1501072..7e10ed4 100644 --- a/lua/conform/formatters/fish_indent.lua +++ b/lua/conform/formatters/fish_indent.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://fishshell.com/docs/current/cmds/fish_indent.html", diff --git a/lua/conform/formatters/gdformat.lua b/lua/conform/formatters/gdformat.lua index 914bb89..d180f12 100644 --- a/lua/conform/formatters/gdformat.lua +++ b/lua/conform/formatters/gdformat.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/Scony/godot-gdscript-toolkit", diff --git a/lua/conform/formatters/gofmt.lua b/lua/conform/formatters/gofmt.lua index b0b81c3..2bfd6b4 100644 --- a/lua/conform/formatters/gofmt.lua +++ b/lua/conform/formatters/gofmt.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://pkg.go.dev/cmd/gofmt", diff --git a/lua/conform/formatters/gofumpt.lua b/lua/conform/formatters/gofumpt.lua index 79fb4dc..2d4f9a5 100644 --- a/lua/conform/formatters/gofumpt.lua +++ b/lua/conform/formatters/gofumpt.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/mvdan/gofumpt", diff --git a/lua/conform/formatters/goimports.lua b/lua/conform/formatters/goimports.lua index 2361e43..64efea6 100644 --- a/lua/conform/formatters/goimports.lua +++ b/lua/conform/formatters/goimports.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://pkg.go.dev/golang.org/x/tools/cmd/goimports", diff --git a/lua/conform/formatters/htmlbeautifier.lua b/lua/conform/formatters/htmlbeautifier.lua index 3182f99..73b9275 100644 --- a/lua/conform/formatters/htmlbeautifier.lua +++ b/lua/conform/formatters/htmlbeautifier.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/threedaymonk/htmlbeautifier", diff --git a/lua/conform/formatters/isort.lua b/lua/conform/formatters/isort.lua index f6c6e3d..9b02c4e 100644 --- a/lua/conform/formatters/isort.lua +++ b/lua/conform/formatters/isort.lua @@ -1,5 +1,5 @@ local util = require("conform.util") ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/PyCQA/isort", diff --git a/lua/conform/formatters/jq.lua b/lua/conform/formatters/jq.lua index 50a905d..061ed03 100644 --- a/lua/conform/formatters/jq.lua +++ b/lua/conform/formatters/jq.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/stedolan/jq", diff --git a/lua/conform/formatters/nixfmt.lua b/lua/conform/formatters/nixfmt.lua index 6c5001a..27029d5 100644 --- a/lua/conform/formatters/nixfmt.lua +++ b/lua/conform/formatters/nixfmt.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/serokell/nixfmt", diff --git a/lua/conform/formatters/nixpkgs_fmt.lua b/lua/conform/formatters/nixpkgs_fmt.lua index 6685fe8..9ca6a8a 100644 --- a/lua/conform/formatters/nixpkgs_fmt.lua +++ b/lua/conform/formatters/nixpkgs_fmt.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/nix-community/nixpkgs-fmt", diff --git a/lua/conform/formatters/ocamlformat.lua b/lua/conform/formatters/ocamlformat.lua index 4ea1b49..0305083 100644 --- a/lua/conform/formatters/ocamlformat.lua +++ b/lua/conform/formatters/ocamlformat.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/ocaml-ppx/ocamlformat", diff --git a/lua/conform/formatters/pg_format.lua b/lua/conform/formatters/pg_format.lua index 87aff66..f016454 100644 --- a/lua/conform/formatters/pg_format.lua +++ b/lua/conform/formatters/pg_format.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/darold/pgFormatter", diff --git a/lua/conform/formatters/prettier.lua b/lua/conform/formatters/prettier.lua index 6f4bbfb..ea45ffb 100644 --- a/lua/conform/formatters/prettier.lua +++ b/lua/conform/formatters/prettier.lua @@ -1,5 +1,5 @@ local util = require("conform.util") ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/prettier/prettier", @@ -7,6 +7,10 @@ return { }, command = util.from_node_modules("prettier"), args = { "--stdin-filepath", "$FILENAME" }, + range_args = function(ctx) + local start_offset, end_offset = util.get_offsets_from_range(ctx.buf, ctx.range) + return { "$FILENAME", "--range-start=" .. start_offset, "--range-end=" .. end_offset } + end, cwd = util.root_file({ -- https://prettier.io/docs/en/configuration.html ".prettierrc", diff --git a/lua/conform/formatters/prettierd.lua b/lua/conform/formatters/prettierd.lua index 0af6baf..3cdc19e 100644 --- a/lua/conform/formatters/prettierd.lua +++ b/lua/conform/formatters/prettierd.lua @@ -1,5 +1,5 @@ local util = require("conform.util") ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/fsouza/prettierd", @@ -7,6 +7,10 @@ return { }, command = util.from_node_modules("prettierd"), args = { "$FILENAME" }, + range_args = function(ctx) + local start_offset, end_offset = util.get_offsets_from_range(ctx.buf, ctx.range) + return { "$FILENAME", "--range-start=" .. start_offset, "--range-end=" .. end_offset } + end, cwd = util.root_file({ -- https://prettier.io/docs/en/configuration.html ".prettierrc", diff --git a/lua/conform/formatters/rubocop.lua b/lua/conform/formatters/rubocop.lua index 492f379..5e2a143 100644 --- a/lua/conform/formatters/rubocop.lua +++ b/lua/conform/formatters/rubocop.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/rubocop/rubocop", diff --git a/lua/conform/formatters/rustfmt.lua b/lua/conform/formatters/rustfmt.lua index 7b5e322..d8f0f19 100644 --- a/lua/conform/formatters/rustfmt.lua +++ b/lua/conform/formatters/rustfmt.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/rust-lang/rustfmt", diff --git a/lua/conform/formatters/scalafmt.lua b/lua/conform/formatters/scalafmt.lua index 2b9e451..ea82624 100644 --- a/lua/conform/formatters/scalafmt.lua +++ b/lua/conform/formatters/scalafmt.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/scalameta/scalafmt", diff --git a/lua/conform/formatters/shfmt.lua b/lua/conform/formatters/shfmt.lua index 6e40f0c..29b8615 100644 --- a/lua/conform/formatters/shfmt.lua +++ b/lua/conform/formatters/shfmt.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/mvdan/sh", diff --git a/lua/conform/formatters/sql_formatter.lua b/lua/conform/formatters/sql_formatter.lua index bd92851..9452a61 100644 --- a/lua/conform/formatters/sql_formatter.lua +++ b/lua/conform/formatters/sql_formatter.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/sql-formatter-org/sql-formatter", diff --git a/lua/conform/formatters/stylua.lua b/lua/conform/formatters/stylua.lua index ac482bd..d971932 100644 --- a/lua/conform/formatters/stylua.lua +++ b/lua/conform/formatters/stylua.lua @@ -1,5 +1,5 @@ local util = require("conform.util") ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/JohnnyMorganz/StyLua", @@ -7,6 +7,19 @@ return { }, command = "stylua", args = { "--search-parent-directories", "--stdin-filepath", "$FILENAME", "-" }, + range_args = function(ctx) + local start_offset, end_offset = util.get_offsets_from_range(ctx.buf, ctx.range) + return { + "--search-parent-directories", + "--stdin-filepath", + "$FILENAME", + "--range-start", + tostring(start_offset), + "--range-end", + tostring(end_offset), + "-", + } + end, cwd = util.root_file({ ".stylua.toml", "stylua.toml", diff --git a/lua/conform/formatters/swift_format.lua b/lua/conform/formatters/swift_format.lua index 2b81297..e3e4365 100644 --- a/lua/conform/formatters/swift_format.lua +++ b/lua/conform/formatters/swift_format.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/apple/swift-format", diff --git a/lua/conform/formatters/swiftformat.lua b/lua/conform/formatters/swiftformat.lua index 821a010..ed142ad 100644 --- a/lua/conform/formatters/swiftformat.lua +++ b/lua/conform/formatters/swiftformat.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/nicklockwood/SwiftFormat", diff --git a/lua/conform/formatters/terraform_fmt.lua b/lua/conform/formatters/terraform_fmt.lua index 44edc55..fba8a12 100644 --- a/lua/conform/formatters/terraform_fmt.lua +++ b/lua/conform/formatters/terraform_fmt.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://www.terraform.io/docs/cli/commands/fmt.html", diff --git a/lua/conform/formatters/uncrustify.lua b/lua/conform/formatters/uncrustify.lua index 3430063..2782e96 100644 --- a/lua/conform/formatters/uncrustify.lua +++ b/lua/conform/formatters/uncrustify.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/uncrustify/uncrustify", diff --git a/lua/conform/formatters/xmlformat.lua b/lua/conform/formatters/xmlformat.lua index 25b48e2..d04c9cc 100644 --- a/lua/conform/formatters/xmlformat.lua +++ b/lua/conform/formatters/xmlformat.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/pamoller/xmlformatter", diff --git a/lua/conform/formatters/yamlfix.lua b/lua/conform/formatters/yamlfix.lua index 1b00e01..7592340 100644 --- a/lua/conform/formatters/yamlfix.lua +++ b/lua/conform/formatters/yamlfix.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/lyz-code/yamlfix", diff --git a/lua/conform/formatters/yamlfmt.lua b/lua/conform/formatters/yamlfmt.lua index 56c6cb6..8f188ab 100644 --- a/lua/conform/formatters/yamlfmt.lua +++ b/lua/conform/formatters/yamlfmt.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/google/yamlfmt", diff --git a/lua/conform/formatters/yapf.lua b/lua/conform/formatters/yapf.lua index 5d7e866..fc3cfeb 100644 --- a/lua/conform/formatters/yapf.lua +++ b/lua/conform/formatters/yapf.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/google/yapf", @@ -6,4 +6,7 @@ return { }, command = "yapf", args = { "--quiet" }, + range_args = function(ctx) + return { "--quiet", "--lines", string.format("%d-%d", ctx.range.start[1], ctx.range["end"][1]) } + end, } diff --git a/lua/conform/formatters/zigfmt.lua b/lua/conform/formatters/zigfmt.lua index 9c93c0b..8097d9c 100644 --- a/lua/conform/formatters/zigfmt.lua +++ b/lua/conform/formatters/zigfmt.lua @@ -1,4 +1,4 @@ ----@type conform.FormatterConfig +---@type conform.FileFormatterConfig return { meta = { url = "https://github.com/ziglang/zig", diff --git a/lua/conform/init.lua b/lua/conform/init.lua index 1bea2ea..9ba56bb 100644 --- a/lua/conform/init.lua +++ b/lua/conform/init.lua @@ -7,26 +7,35 @@ local M = {} ---@field available boolean ---@field available_msg? string ----@class (exact) conform.StaticFormatterConfig +---@class (exact) conform.FormatterConfig ---@field command string|fun(ctx: conform.Context): string ---@field args? string[]|fun(ctx: conform.Context): string[] +---@field range_args? fun(ctx: conform.RangeContext): string[] ---@field cwd? fun(ctx: conform.Context): nil|string ---@field require_cwd? boolean When cwd is not found, don't run the formatter (default false) ---@field stdin? boolean Send buffer contents to stdin (default true) ---@field condition? fun(ctx: conform.Context): boolean ---@field exit_codes? integer[] Exit codes that indicate success (default {0}) ----@class (exact) conform.FormatterConfig : conform.StaticFormatterConfig +---@class (exact) conform.FileFormatterConfig : conform.FormatterConfig ---@field meta conform.FormatterMeta ---@class (exact) conform.FormatterMeta ---@field url string ---@field description string ---- + ---@class (exact) conform.Context ---@field buf integer ---@field filename string ---@field dirname string +---@field range? conform.Range + +---@class (exact) conform.RangeContext : conform.Context +---@field range conform.Range + +---@class (exact) conform.Range +---@field start integer[] +---@field end integer[] ---@class (exact) conform.RunOptions ---@field run_all_formatters nil|boolean Run all listed formatters instead of stopping at the first one. @@ -37,7 +46,7 @@ local M = {} ---@type table<string, string[]|conform.FormatterList> M.formatters_by_ft = {} ----@type table<string, conform.StaticFormatterConfig|fun(): conform.StaticFormatterConfig> +---@type table<string, conform.FormatterConfig|fun(): conform.FormatterConfig> M.formatters = {} M.setup = function(opts) @@ -167,6 +176,37 @@ local function filter_formatters(formatters, run_options) return all_info end +---@param bufnr integer +---@param mode "v"|"V" +---@return table {start={row,col}, end={row,col}} using (1, 0) indexing +local function range_from_selection(bufnr, mode) + -- [bufnum, lnum, col, off]; both row and column 1-indexed + local start = vim.fn.getpos("v") + local end_ = vim.fn.getpos(".") + local start_row = start[2] + local start_col = start[3] + local end_row = end_[2] + local end_col = end_[3] + + -- A user can start visual selection at the end and move backwards + -- Normalize the range to start < end + if start_row == end_row and end_col < start_col then + end_col, start_col = start_col, end_col + elseif end_row < start_row then + start_row, end_row = end_row, start_row + start_col, end_col = end_col, start_col + end + if mode == "V" then + start_col = 1 + local lines = vim.api.nvim_buf_get_lines(bufnr, end_row - 1, end_row, true) + end_col = #lines[1] + end + return { + ["start"] = { start_row, start_col - 1 }, + ["end"] = { end_row, end_col - 1 }, + } +end + ---Format a buffer ---@param opts? table --- timeout_ms nil|integer Time in milliseconds to block for formatting. Defaults to 1000. No effect if async = true. @@ -175,9 +215,10 @@ end --- formatters nil|string[] List of formatters to run. Defaults to all formatters for the buffer filetype. --- lsp_fallback nil|boolean Attempt LSP formatting if no formatters are available. Defaults to false. --- quiet nil|boolean Don't show any notifications for warnings or failures. Defaults to false. +--- range nil|table Range to format. Table must contain `start` and `end` keys with {row, col} tuples using (1,0) indexing. Defaults to current selection in visual mode ---@return boolean True if any formatters were attempted M.format = function(opts) - ---@type {timeout_ms: integer, bufnr: integer, async: boolean, lsp_fallback: boolean, quiet: boolean, formatters?: string[]} + ---@type {timeout_ms: integer, bufnr: integer, async: boolean, lsp_fallback: boolean, quiet: boolean, formatters?: string[], range?: conform.Range} opts = vim.tbl_extend("keep", opts or {}, { timeout_ms = 1000, bufnr = 0, @@ -219,10 +260,21 @@ M.format = function(opts) local any_formatters = not vim.tbl_isempty(formatters) if any_formatters then + local mode = vim.api.nvim_get_mode().mode + if not opts.range and mode == "v" or mode == "V" then + opts.range = range_from_selection(opts.bufnr, mode) + end + if opts.async then - require("conform.runner").format_async(opts.bufnr, formatters) + require("conform.runner").format_async(opts.bufnr, formatters, opts.range) else - require("conform.runner").format_sync(opts.bufnr, formatters, opts.timeout_ms, opts.quiet) + require("conform.runner").format_sync( + opts.bufnr, + formatters, + opts.timeout_ms, + opts.quiet, + opts.range + ) end elseif opts.lsp_fallback and supports_lsp_format(opts.bufnr) then log.debug("Running LSP formatter on %s", vim.api.nvim_buf_get_name(opts.bufnr)) @@ -276,7 +328,7 @@ end ---@private ---@param formatter string ----@return nil|conform.StaticFormatterConfig +---@return nil|conform.FormatterConfig M.get_formatter_config = function(formatter) local config = M.formatters[formatter] if not config then diff --git a/lua/conform/runner.lua b/lua/conform/runner.lua index 7ccaec6..843dae1 100644 --- a/lua/conform/runner.lua +++ b/lua/conform/runner.lua @@ -5,35 +5,51 @@ local uv = vim.uv or vim.loop local M = {} ---@param ctx conform.Context ----@param config conform.StaticFormatterConfig +---@param config conform.FormatterConfig M.build_cmd = function(ctx, config) local command = config.command if type(command) == "function" then command = command(ctx) end local cmd = { command } - if config.args then - local args = config.args + local args = {} + if ctx.range and config.range_args then + ---@cast ctx conform.RangeContext + args = config.range_args(ctx) + elseif config.args then if type(config.args) == "function" then args = config.args(ctx) + else + ---@diagnostic disable-next-line: cast-local-type + args = config.args end - ---@cast args string[] - for _, v in ipairs(args) do - if v == "$FILENAME" then - v = ctx.filename - elseif v == "$DIRNAME" then - v = ctx.dirname - end - table.insert(cmd, v) + end + + ---@diagnostic disable-next-line: param-type-mismatch + for _, v in ipairs(args) do + if v == "$FILENAME" then + v = ctx.filename + elseif v == "$DIRNAME" then + v = ctx.dirname end + table.insert(cmd, v) end return cmd end +---@param range? conform.Range +---@param start_a integer +---@param end_a integer +local function indices_in_range(range, start_a, end_a) + return not range or (start_a <= range["end"][1] and range["start"][1] <= end_a) +end + ---@param bufnr integer ---@param original_lines string[] ---@param new_lines string[] -local function apply_format(bufnr, original_lines, new_lines) +---@param range? conform.Range +---@param only_apply_range boolean +local function apply_format(bufnr, original_lines, new_lines, range, only_apply_range) local original_text = table.concat(original_lines, "\n") -- Trim off the final newline from the formatted text because that is baked in to -- the vim lines representation @@ -68,18 +84,21 @@ local function apply_format(bufnr, original_lines, new_lines) count_b = count_b + 1 end local replacement = util.tbl_slice(new_lines, start_b, start_b + count_b - 1) - vim.api.nvim_buf_set_lines(bufnr, start_a - 1, start_a - 1 + count_a, true, replacement) + local end_a = start_a + count_a + if not only_apply_range or indices_in_range(range, start_a, end_a) then + vim.api.nvim_buf_set_lines(bufnr, start_a - 1, end_a - 1, true, replacement) + end end end ---@param bufnr integer ---@param formatter conform.FormatterInfo +---@param config conform.FormatterConfig +---@param ctx conform.Context ---@param input_lines string[] ---@param callback fun(err?: string, output?: string[]) ----@return integer -local function run_formatter(bufnr, formatter, input_lines, callback) - local config = assert(require("conform").get_formatter_config(formatter.name)) - local ctx = M.build_context(bufnr, config) +---@return integer job_id +local function run_formatter(bufnr, formatter, config, ctx, input_lines, callback) local cmd = M.build_cmd(ctx, config) local cwd = nil if config.cwd then @@ -159,9 +178,10 @@ local function run_formatter(bufnr, formatter, input_lines, callback) end ---@param bufnr integer ----@param config conform.StaticFormatterConfig +---@param config conform.FormatterConfig +---@param range? conform.Range ---@return conform.Context -M.build_context = function(bufnr, config) +M.build_context = function(bufnr, config, range) if bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end @@ -193,13 +213,15 @@ M.build_context = function(bufnr, config) buf = bufnr, filename = filename, dirname = dirname, + range = range, } end ---@param bufnr integer ---@param formatters conform.FormatterInfo[] +---@param range? conform.Range ---@param callback? fun(err?: string) -M.format_async = function(bufnr, formatters, callback) +M.format_async = function(bufnr, formatters, range, callback) if bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end @@ -207,6 +229,7 @@ M.format_async = function(bufnr, formatters, callback) local changedtick = vim.b[bufnr].changedtick local original_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) local input_lines = original_lines + local all_support_range_formatting = true -- kill previous jobs for buffer local prev_jid = vim.b[bufnr].conform_jid @@ -221,7 +244,7 @@ M.format_async = function(bufnr, formatters, callback) if not formatter then -- discard formatting if buffer has changed if vim.b[bufnr].changedtick == changedtick then - apply_format(bufnr, original_lines, input_lines) + apply_format(bufnr, original_lines, input_lines, range, not all_support_range_formatting) else log.info( "Async formatter discarding changes for %s: concurrent modification", @@ -235,8 +258,10 @@ M.format_async = function(bufnr, formatters, callback) end idx = idx + 1 + local config = assert(require("conform").get_formatter_config(formatter.name)) + local ctx = M.build_context(bufnr, config, range) local jid - jid = run_formatter(bufnr, formatter, input_lines, function(err, output) + jid = run_formatter(bufnr, formatter, config, ctx, input_lines, function(err, output) if err then -- Only log the error if the job wasn't canceled if vim.api.nvim_buf_is_valid(bufnr) and jid == vim.b[bufnr].conform_jid then @@ -250,6 +275,7 @@ M.format_async = function(bufnr, formatters, callback) input_lines = output run_next_formatter() end) + all_support_range_formatting = all_support_range_formatting and config.range_args ~= nil end run_next_formatter() end @@ -258,7 +284,8 @@ end ---@param formatters conform.FormatterInfo[] ---@param timeout_ms integer ---@param quiet boolean -M.format_sync = function(bufnr, formatters, timeout_ms, quiet) +---@param range? conform.Range +M.format_sync = function(bufnr, formatters, timeout_ms, quiet, range) if bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end @@ -274,6 +301,7 @@ M.format_sync = function(bufnr, formatters, timeout_ms, quiet) end end + local all_support_range_formatting = true for _, formatter in ipairs(formatters) do local remaining = timeout_ms - (uv.hrtime() / 1e6 - start) if remaining <= 0 then @@ -286,13 +314,16 @@ M.format_sync = function(bufnr, formatters, timeout_ms, quiet) end local done = false local result = nil - run_formatter(bufnr, formatter, input_lines, function(err, output) + local config = assert(require("conform").get_formatter_config(formatter.name)) + local ctx = M.build_context(bufnr, config, range) + local jid = run_formatter(bufnr, formatter, config, ctx, input_lines, function(err, output) if err then log.error(err) end done = true result = output end) + all_support_range_formatting = all_support_range_formatting and config.range_args ~= nil local wait_result, wait_reason = vim.wait(remaining, function() return done @@ -306,6 +337,7 @@ M.format_sync = function(bufnr, formatters, timeout_ms, quiet) vim.notify(string.format("Formatter '%s' timed out", formatter.name), vim.log.levels.WARN) end end + vim.fn.jobstop(jid) return end @@ -317,7 +349,7 @@ M.format_sync = function(bufnr, formatters, timeout_ms, quiet) end local final_result = input_lines - apply_format(bufnr, original_lines, final_result) + apply_format(bufnr, original_lines, final_result, range, not all_support_range_formatting) end return M diff --git a/lua/conform/util.lua b/lua/conform/util.lua index 353c447..d2ca1b9 100644 --- a/lua/conform/util.lua +++ b/lua/conform/util.lua @@ -58,6 +58,20 @@ M.save_win_positions = function(bufnr) end end +---@param bufnr integer +---@param range conform.Range +---@return integer start_offset +---@return integer end_offset +M.get_offsets_from_range = function(bufnr, range) + local row = range.start[1] - 1 + local end_row = range["end"][1] - 1 + local col = range.start[2] + local end_col = range["end"][2] + local start_offset = vim.api.nvim_buf_get_offset(bufnr, row) + col + local end_offset = vim.api.nvim_buf_get_offset(bufnr, end_row) + end_col + return start_offset, end_offset +end + ---@generic T : any ---@param tbl T[] ---@param start_idx? number diff --git a/tests/options_doc.lua b/tests/options_doc.lua index 4360b92..6b215c2 100644 --- a/tests/options_doc.lua +++ b/tests/options_doc.lua @@ -30,6 +30,10 @@ require("conform").setup({ -- OPTIONAL - all fields below this are optional -- A list of strings, or a function that returns a list of strings args = { "--stdin-from-filename", "$FILENAME" }, + -- If the formatter supports range formatting, create the range arguments here + range_args = function(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) -- When false, will create a temp file (will appear in "$FILENAME" args). The temp -- file is assumed to be modified in-place by the format command. |