diff options
-rw-r--r-- | README.md | 60 | ||||
-rw-r--r-- | doc/conform.txt | 113 | ||||
-rw-r--r-- | doc/recipes.md | 32 | ||||
-rw-r--r-- | lua/conform/formatters/dcm_format.lua | 2 | ||||
-rw-r--r-- | lua/conform/formatters/injected.lua | 26 | ||||
-rw-r--r-- | lua/conform/health.lua | 11 | ||||
-rw-r--r-- | lua/conform/init.lua | 261 | ||||
-rw-r--r-- | lua/conform/types.lua | 44 | ||||
-rw-r--r-- | scripts/options_doc.lua | 12 | ||||
-rw-r--r-- | tests/injected_spec.lua | 5 |
10 files changed, 402 insertions, 164 deletions
@@ -18,16 +18,16 @@ Lightweight yet powerful formatter plugin for Neovim - [setup(opts)](#setupopts) - [format(opts, callback)](#formatopts-callback) - [list_formatters(bufnr)](#list_formattersbufnr) + - [list_formatters_to_run(bufnr)](#list_formatters_to_runbufnr) - [list_all_formatters()](#list_all_formatters) - [get_formatter_info(formatter, bufnr)](#get_formatter_infoformatter-bufnr) - - [will_fallback_lsp(options)](#will_fallback_lspoptions) - [Acknowledgements](#acknowledgements) <!-- /TOC --> ## Requirements -- Neovim 0.8+ +- Neovim 0.9+ (for older versions, use a [nvim-0.x branch](https://github.com/stevearc/conform.nvim/branches)) ## Features @@ -129,8 +129,10 @@ require("conform").setup({ lua = { "stylua" }, -- Conform will run multiple formatters sequentially python = { "isort", "black" }, - -- Use a sub-list to run only the first available formatter - javascript = { { "prettierd", "prettier" } }, + -- You can customize some of the format options for the filetype (:help conform.format) + rust = { "rustfmt", lsp_format = "fallback" }, + -- Conform will run the first available formatter + javascript = { "prettierd", "prettier", stop_after_first = true }, }, }) ``` @@ -214,7 +216,7 @@ You can view this list in vim with `:help conform-formatters` - [darker](https://github.com/akaihola/darker) - Run black only on changed lines. - [dart_format](https://dart.dev/tools/dart-format) - Replace the whitespace in your program with formatting that follows Dart guidelines. - [dcm_fix](https://dcm.dev/docs/cli/formatting/fix/) - Fixes issues produced by dcm analyze, dcm check-unused-code or dcm check-dependencies commands. -- [dcm_format](https://dcm.dev/docs/cli/formatting/format/) - Formats *.dart files. +- [dcm_format](https://dcm.dev/docs/cli/formatting/format/) - Formats .dart files. - [deno_fmt](https://deno.land/manual/tools/formatter) - Use [Deno](https://deno.land/) to format TypeScript, JavaScript/JSON and markdown. - [dfmt](https://github.com/dlang-community/dfmt) - Formatter for D source code. - [djlint](https://github.com/Riverside-Healthcare/djLint) - ✨ HTML Template Linter and Formatter. Django - Jinja - Nunjucks - Handlebars - GoLang. @@ -431,6 +433,7 @@ require("conform").formatters.shfmt = { - [Command to toggle format-on-save](doc/recipes.md#command-to-toggle-format-on-save) - [Automatically run slow formatters async](doc/recipes.md#automatically-run-slow-formatters-async) - [Lazy loading with lazy.nvim](doc/recipes.md#lazy-loading-with-lazynvim) +- [Leave visual mode after range format](doc/recipes.md#leave-visual-mode-after-range-format) <!-- /RECIPES --> @@ -457,8 +460,8 @@ require("conform").setup({ lua = { "stylua" }, -- Conform will run multiple formatters sequentially go = { "goimports", "gofmt" }, - -- Use a sub-list to run only the first available formatter - javascript = { { "prettierd", "prettier" } }, + -- You can also customize some of the format options for the filetype + rust = { "rustfmt", lsp_format = "fallback" }, -- You can use a function here to determine the formatters dynamically python = function(bufnr) if require("conform").get_formatter_info("ruff_format", bufnr).available then @@ -473,6 +476,11 @@ require("conform").setup({ -- have other formatters configured. ["_"] = { "trim_whitespace" }, }, + -- Set this to change the default values when calling conform.format() + -- This will also affect the default values for format_on_save/format_after_save + default_format_opts = { + lsp_format = "fallback", + }, -- If this is set, Conform will run the formatter on save. -- It will pass the table to conform.format(). -- This can also be a function that returns the table. @@ -491,6 +499,8 @@ require("conform").setup({ log_level = vim.log.levels.ERROR, -- Conform will notify you when a formatter errors notify_on_error = true, + -- Conform will notify you when no formatters are available for the buffer + notify_no_formatters = true, -- Custom formatters and overrides for built-in formatters formatters = { my_formatter = { @@ -527,7 +537,6 @@ require("conform").setup({ -- Set to false to disable merging the config with the base definition inherit = true, -- When inherit = true, add these additional arguments to the beginning of the command. - -- When inherit = true, add these additional arguments to the command. -- This can also be a function, like args prepend_args = { "--use-tabs" }, -- When inherit = true, add these additional arguments to the end of the command. @@ -576,9 +585,11 @@ require("conform").formatters.my_formatter = { | opts | `nil\|conform.setupOpts` | | | | | formatters_by_ft | `nil\|table<string, conform.FiletypeFormatter>` | Map of filetype to formatters | | | format_on_save | `nil\|conform.FormatOpts\|fun(bufnr: integer): nil\|conform.FormatOpts` | If this is set, Conform will run the formatter on save. It will pass the table to conform.format(). This can also be a function that returns the table. | +| | default_format_opts | `nil\|conform.DefaultFormatOpts` | The default options to use when calling conform.format() | | | format_after_save | `nil\|conform.FormatOpts\|fun(bufnr: integer): nil\|conform.FormatOpts` | If this is set, Conform will run the formatter asynchronously after save. It will pass the table to conform.format(). This can also be a function that returns the table. | | | log_level | `nil\|integer` | Set the log level (e.g. `vim.log.levels.DEBUG`). Use `:ConformInfo` to see the location of the log file. | | | notify_on_error | `nil\|boolean` | Conform will notify you when a formatter errors (default true). | +| | notify_no_formatters | `nil\|boolean` | Conform will notify you when no formatters are available for the buffer (default true). | | | formatters | `nil\|table<string, conform.FormatterConfigOverride\|fun(bufnr: integer): nil\|conform.FormatterConfigOverride>` | Custom formatters and overrides for built-in formatters. | ### format(opts, callback) @@ -601,8 +612,9 @@ Format a buffer | | | > `"prefer"` | use only LSP formatting when available | | | | > `"first"` | LSP formatting is used when available and then other formatters | | | | > `"last"` | other formatters are used then LSP formatting when available | +| | stop_after_first | `nil\|boolean` | Only run the first available formatter in the list. 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 | +| | range | `nil\|conform.Range` | 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 | | | id | `nil\|integer` | Passed to vim.lsp.buf.format when using LSP formatting | | | name | `nil\|string` | Passed to vim.lsp.buf.format when using LSP formatting | | | filter | `nil\|fun(client: table): boolean` | Passed to vim.lsp.buf.format when using LSP formatting | @@ -623,6 +635,27 @@ Retrieve the available formatters for a buffer | ----- | -------------- | ---- | | bufnr | `nil\|integer` | | +### list_formatters_to_run(bufnr) + +`list_formatters_to_run(bufnr): conform.FormatterInfo[], boolean` \ +Get the exact formatters that will be run for a buffer. + +| Param | Type | Desc | +| ----- | -------------- | ---- | +| bufnr | `nil\|integer` | | + +Returns: + +| Type | Desc | +| ----------------------- | -------------------------- | +| conform.FormatterInfo[] | | +| boolean | lsp Will use LSP formatter | + +**Note:** +<pre> +This accounts for stop_after_first, lsp fallback logic, etc. +</pre> + ### list_all_formatters() `list_all_formatters(): conform.FormatterInfo[]` \ @@ -638,15 +671,6 @@ Get information about a formatter (including availability) | --------- | -------------- | ------------------------- | | formatter | `string` | The name of the formatter | | bufnr | `nil\|integer` | | - -### will_fallback_lsp(options) - -`will_fallback_lsp(options): boolean` \ -Check if the buffer will use LSP formatting when lsp_format = "fallback" - -| Param | Type | Desc | -| ------- | ------------ | ------------------------------------ | -| options | `nil\|table` | Options passed to vim.lsp.buf.format | <!-- /API --> ## Acknowledgements diff --git a/doc/conform.txt b/doc/conform.txt index 3f6568f..5af52f8 100644 --- a/doc/conform.txt +++ b/doc/conform.txt @@ -17,8 +17,8 @@ OPTIONS *conform-option lua = { "stylua" }, -- Conform will run multiple formatters sequentially go = { "goimports", "gofmt" }, - -- Use a sub-list to run only the first available formatter - javascript = { { "prettierd", "prettier" } }, + -- You can also customize some of the format options for the filetype + rust = { "rustfmt", lsp_format = "fallback" }, -- You can use a function here to determine the formatters dynamically python = function(bufnr) if require("conform").get_formatter_info("ruff_format", bufnr).available then @@ -33,6 +33,11 @@ OPTIONS *conform-option -- have other formatters configured. ["_"] = { "trim_whitespace" }, }, + -- Set this to change the default values when calling conform.format() + -- This will also affect the default values for format_on_save/format_after_save + default_format_opts = { + lsp_format = "fallback", + }, -- If this is set, Conform will run the formatter on save. -- It will pass the table to conform.format(). -- This can also be a function that returns the table. @@ -51,6 +56,8 @@ OPTIONS *conform-option log_level = vim.log.levels.ERROR, -- Conform will notify you when a formatter errors notify_on_error = true, + -- Conform will notify you when no formatters are available for the buffer + notify_no_formatters = true, -- Custom formatters and overrides for built-in formatters formatters = { my_formatter = { @@ -87,7 +94,6 @@ OPTIONS *conform-option -- Set to false to disable merging the config with the base definition inherit = true, -- When inherit = true, add these additional arguments to the beginning of the command. - -- When inherit = true, add these additional arguments to the command. -- This can also be a function, like args prepend_args = { "--use-tabs" }, -- When inherit = true, add these additional arguments to the end of the command. @@ -123,6 +129,26 @@ setup({opts}) *conform.setu If this is set, Conform will run the formatter on save. It will pass the table to conform.format(). This can also be a function that returns the table. + {default_format_opts} `nil|conform.DefaultFormatOpts` The default + options to use when calling conform.format() + {timeout_ms} `nil|integer` Time in milliseconds to block for + formatting. Defaults to 1000. No effect if + async = true. + {lsp_format} `nil|conform.LspFormatOpts` Configure if and + when LSP should be used for formatting. + Defaults to "never". + `"never"` never use the LSP for formatting (default) + `"fallback"` LSP formatting is used when no other formatters + are available + `"prefer"` use only LSP formatting when available + `"first"` LSP formatting is used when available and then + other formatters + `"last"` other formatters are used then LSP formatting + when available + {quiet} `nil|boolean` Don't show any notifications for + warnings or failures. Defaults to false. + {stop_after_first} `nil|boolean` Only run the first available + formatter in the list. Defaults to false. {format_after_save} `nil|conform.FormatOpts|fun(bufnr: integer): nil|conform.FormatOpts` If this is set, Conform will run the formatter asynchronously after save. It will pass the table @@ -133,6 +159,9 @@ setup({opts}) *conform.setu the location of the log file. {notify_on_error} `nil|boolean` Conform will notify you when a formatter errors (default true). + {notify_no_formatters} `nil|boolean` Conform will notify you when no + formatters are available for the buffer (default + true). {formatters} `nil|table<string, conform.FormatterConfigOverride|fun(bufnr: integer): nil|conform.FormatterConfigOverride>` Custom formatters and overrides for built-in formatters. @@ -142,20 +171,23 @@ format({opts}, {callback}): boolean *conform.forma Parameters: {opts} `nil|conform.FormatOpts` - {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. If the buffer is modified before the formatter - completes, the formatting will be discarded. - {dry_run} `nil|boolean` If true don't apply formatting changes to - the buffer - {undojoin} `nil|boolean` Use undojoin to merge formatting changes - with previous edit (default false) - {formatters} `nil|string[]` List of formatters to run. Defaults to all - formatters for the buffer filetype. - {lsp_format} `nil|conform.LspFormatOpts` Configure if and when LSP - should be used for formatting. Defaults to "never". + {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. If the buffer is modified before + the formatter completes, the formatting will be + discarded. + {dry_run} `nil|boolean` If true don't apply formatting + changes to the buffer + {undojoin} `nil|boolean` Use undojoin to merge formatting + changes with previous edit (default false) + {formatters} `nil|string[]` List of formatters to run. Defaults + to all formatters for the buffer filetype. + {lsp_format} `nil|conform.LspFormatOpts` Configure if and when + LSP should be used for formatting. Defaults to + "never". `"never"` never use the LSP for formatting (default) `"fallback"` LSP formatting is used when no other formatters are available @@ -164,17 +196,22 @@ format({opts}, {callback}): boolean *conform.forma formatters `"last"` other formatters are used then LSP formatting when available - {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 - {id} `nil|integer` Passed to |vim.lsp.buf.format| when using - LSP formatting - {name} `nil|string` Passed to |vim.lsp.buf.format| when using - LSP formatting - {filter} `nil|fun(client: table): boolean` Passed to - |vim.lsp.buf.format| when using LSP formatting + {stop_after_first} `nil|boolean` Only run the first available + formatter in the list. Defaults to false. + {quiet} `nil|boolean` Don't show any notifications for + warnings or failures. Defaults to false. + {range} `nil|conform.Range` 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 + {start} `integer[]` + {end} `integer[]` + {id} `nil|integer` Passed to |vim.lsp.buf.format| when + using LSP formatting + {name} `nil|string` Passed to |vim.lsp.buf.format| when + using LSP formatting + {filter} `nil|fun(client: table): boolean` Passed to |vim.ls + p.buf.format| when using LSP formatting {callback} `nil|fun(err: nil|string, did_edit: nil|boolean)` Called once formatting has completed Returns: @@ -186,6 +223,18 @@ list_formatters({bufnr}): conform.FormatterInfo[] *conform.list_formatter Parameters: {bufnr} `nil|integer` +list_formatters_to_run({bufnr}): conform.FormatterInfo[], boolean *conform.list_formatters_to_run* + Get the exact formatters that will be run for a buffer. + + Parameters: + {bufnr} `nil|integer` + Returns: + `conform.FormatterInfo[]` + `boolean` lsp Will use LSP formatter + + Note: + This accounts for stop_after_first, lsp fallback logic, etc. + list_all_formatters(): conform.FormatterInfo[] *conform.list_all_formatters* List information about all filetype-configured formatters @@ -197,12 +246,6 @@ get_formatter_info({formatter}, {bufnr}): conform.FormatterInfo *conform.get_for {formatter} `string` The name of the formatter {bufnr} `nil|integer` -will_fallback_lsp({options}): boolean *conform.will_fallback_lsp* - Check if the buffer will use LSP formatting when lsp_format = "fallback" - - Parameters: - {options} `nil|table` Options passed to |vim.lsp.buf.format| - -------------------------------------------------------------------------------- FORMATTERS *conform-formatters* @@ -258,7 +301,7 @@ FORMATTERS *conform-formatter follows Dart guidelines. `dcm_fix` - Fixes issues produced by dcm analyze, dcm check-unused-code or dcm check-dependencies commands. -`dcm_format` - Formats *.dart files. +`dcm_format` - Formats .dart files. `deno_fmt` - Use [Deno](https://deno.land/) to format TypeScript, JavaScript/JSON and markdown. `dfmt` - Formatter for D source code. diff --git a/doc/recipes.md b/doc/recipes.md index f5d6f99..0c085f2 100644 --- a/doc/recipes.md +++ b/doc/recipes.md @@ -7,6 +7,7 @@ - [Command to toggle format-on-save](#command-to-toggle-format-on-save) - [Automatically run slow formatters async](#automatically-run-slow-formatters-async) - [Lazy loading with lazy.nvim](#lazy-loading-with-lazynvim) +- [Leave visual mode after range format](#leave-visual-mode-after-range-format) <!-- /TOC --> @@ -149,22 +150,28 @@ return { -- Customize or remove this keymap to your liking "<leader>f", function() - require("conform").format({ async = true, lsp_format = "fallback" }) + require("conform").format({ async = true }) end, mode = "", desc = "Format buffer", }, }, - -- Everything in opts will be passed to setup() + -- This will provide type hinting with LuaLS + ---@module "conform" + ---@type conform.setupOpts opts = { -- Define your formatters formatters_by_ft = { lua = { "stylua" }, python = { "isort", "black" }, - javascript = { { "prettierd", "prettier" } }, + javascript = { "prettierd", "prettier", stop_after_first = true }, + }, + -- Set default options + default_format_opts = { + lsp_format = "fallback", }, -- Set up format-on-save - format_on_save = { timeout_ms = 500, lsp_format = "fallback" }, + format_on_save = { timeout_ms = 500 }, -- Customize formatters formatters = { shfmt = { @@ -178,3 +185,20 @@ return { end, } ``` + +## Leave visual mode after range format + +If you call `conform.format` when in visual mode, conform will perform a range format on the selected region. If you want it to leave visual mode afterwards (similar to the default `gw` or `gq` behavior), use this mapping: + +```lua +vim.keymap.set("", "<leader>f", function() + require("conform").format({ async = true }, function(err) + if not err then + local mode = vim.api.nvim_get_mode().mode + if vim.startswith(string.lower(mode), "v") then + vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<Esc>", true, false, true), "n", true) + end + end + end) +end, { desc = "Format code" }) +``` diff --git a/lua/conform/formatters/dcm_format.lua b/lua/conform/formatters/dcm_format.lua index f2af0ac..04adf67 100644 --- a/lua/conform/formatters/dcm_format.lua +++ b/lua/conform/formatters/dcm_format.lua @@ -2,7 +2,7 @@ return { meta = { url = "https://dcm.dev/docs/cli/formatting/format/", - description = "Formats *.dart files.", + description = "Formats .dart files.", }, command = "dcm", args = { "format", "$FILENAME" }, diff --git a/lua/conform/formatters/injected.lua b/lua/conform/formatters/injected.lua index 4dbf1eb..bd3313b 100644 --- a/lua/conform/formatters/injected.lua +++ b/lua/conform/formatters/injected.lua @@ -136,12 +136,6 @@ return { -- (defaults to the value from formatters_by_ft) lang_to_formatters = {}, }, - condition = function(self, ctx) - local ok, parser = pcall(vim.treesitter.get_parser, ctx.buf) - -- Require Neovim 0.9 because the treesitter API has changed significantly - ---@diagnostic disable-next-line: invisible - return ok and parser._injection_query and vim.fn.has("nvim-0.9") == 1 - end, format = function(self, ctx, lines, callback) local conform = require("conform") local errors = require("conform.errors") @@ -284,13 +278,21 @@ return { ---@type string[] local formatter_names if type(ft_formatters) == "function" then - formatter_names = ft_formatters(ctx.buf) - else - local formatters = require("conform").resolve_formatters(ft_formatters, ctx.buf, false) - formatter_names = vim.tbl_map(function(f) - return f.name - end, formatters) + ft_formatters = ft_formatters(ctx.buf) + end + local stop_after_first = ft_formatters.stop_after_first + if stop_after_first == nil then + stop_after_first = conform.default_format_opts.stop_after_first end + if stop_after_first == nil then + stop_after_first = false + end + + local formatters = + conform.resolve_formatters(ft_formatters, ctx.buf, false, stop_after_first) + formatter_names = vim.tbl_map(function(f) + return f.name + end, formatters) local idx = num_format log.debug("Injected format %s:%d:%d: %s", lang, start_lnum, end_lnum, formatter_names) log.trace("Injected format lines %s", input_lines) diff --git a/lua/conform/health.lua b/lua/conform/health.lua index 3ee7567..07ffa22 100644 --- a/lua/conform/health.lua +++ b/lua/conform/health.lua @@ -6,7 +6,6 @@ local health_start = vim.health.start or vim.health.report_start local health_warn = vim.health.warn or vim.health.report_warn local health_info = vim.health.info or vim.health.report_info local health_ok = vim.health.ok or vim.health.report_ok -local islist = vim.islist or vim.tbl_islist ---@param name string ---@return string[] @@ -16,14 +15,6 @@ local function get_formatter_filetypes(name) for filetype, formatters in pairs(conform.formatters_by_ft) do if type(formatters) == "function" then formatters = formatters(0) - -- support the old structure where formatters could be a subkey - elseif not islist(formatters) then - vim.notify_once( - "Using deprecated structure for formatters_by_ft. See :help conform-options for details.", - vim.log.levels.ERROR - ) - ---@diagnostic disable-next-line: undefined-field - formatters = formatters.formatters end for _, ft_name in ipairs(formatters) do @@ -61,7 +52,7 @@ M.check = function() end end ----@param formatters conform.FormatterUnit[] +---@param formatters conform.FiletypeFormatterInternal ---@return string[] local function flatten_formatters(formatters) local flat = {} diff --git a/lua/conform/init.lua b/lua/conform/init.lua index f0b2dfd..c174dea 100644 --- a/lua/conform/init.lua +++ b/lua/conform/init.lua @@ -1,5 +1,3 @@ ----@diagnostic disable-next-line: deprecated -local islist = vim.islist or vim.tbl_islist local M = {} ---@type table<string, conform.FiletypeFormatter> @@ -9,20 +7,72 @@ M.formatters_by_ft = {} M.formatters = {} M.notify_on_error = true +M.notify_no_formatters = true + +---@type conform.DefaultFormatOpts +M.default_format_opts = {} + +-- Defer notifications because nvim-notify can throw errors if called immediately +-- in some contexts (e.g. inside statusline function) +local notify = vim.schedule_wrap(function(...) + vim.notify(...) +end) +local notify_once = vim.schedule_wrap(function(...) + vim.notify_once(...) +end) + +local allowed_default_opts = { "timeout_ms", "lsp_format", "quiet", "stop_after_first" } +local function merge_default_opts(a, b) + for _, key in ipairs(allowed_default_opts) do + if a[key] == nil then + a[key] = b[key] + end + end + return a +end + +---@param conf? conform.FiletypeFormatter +local function check_for_default_opts(conf) + if not conf or type(conf) ~= "table" then + return + end + for k in pairs(conf) do + if type(k) == "string" then + notify( + string.format( + 'conform.setup: the "_" and "*" keys in formatters_by_ft do not support configuring format options, such as "%s"', + k + ), + vim.log.levels.WARN + ) + break + end + end +end ---@param opts? conform.setupOpts M.setup = function(opts) + if vim.fn.has("nvim-0.9") == 0 then + notify("conform.nvim requires Neovim 0.9+", vim.log.levels.ERROR) + return + end opts = opts or {} M.formatters = vim.tbl_extend("force", M.formatters, opts.formatters or {}) M.formatters_by_ft = vim.tbl_extend("force", M.formatters_by_ft, opts.formatters_by_ft or {}) + check_for_default_opts(M.formatters_by_ft["_"]) + check_for_default_opts(M.formatters_by_ft["*"]) + M.default_format_opts = + vim.tbl_extend("force", M.default_format_opts, opts.default_format_opts or {}) if opts.log_level then require("conform.log").level = opts.log_level end - local notify_on_error = opts.notify_on_error - if notify_on_error ~= nil then - M.notify_on_error = notify_on_error + if opts.notify_on_error ~= nil then + M.notify_on_error = opts.notify_on_error + end + if opts.notify_no_formatters ~= nil then + M.notify_no_formatters = opts.notify_no_formatters end local aug = vim.api.nvim_create_augroup("Conform", { clear = true }) @@ -44,7 +94,7 @@ M.setup = function(opts) end if format_args then if format_args.async then - vim.notify_once( + notify_once( "Conform format_on_save cannot use async=true. Use format_after_save instead.", vim.log.levels.ERROR ) @@ -97,7 +147,7 @@ M.setup = function(opts) exit_timeout = format_args.timeout_ms or exit_timeout num_running_format_jobs = num_running_format_jobs + 1 if format_args.async == false then - vim.notify_once( + notify_once( "Conform format_after_save cannot use async=false. Use format_on_save instead.", vim.log.levels.ERROR ) @@ -174,7 +224,7 @@ end ---Get the configured formatter filetype for a buffer ---@param bufnr? integer ----@return nil|string filetype or nil if no formatter is configured +---@return nil|string filetype or nil if no formatter is configured. Can be "_". local function get_matching_filetype(bufnr) if not bufnr or bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() @@ -193,7 +243,7 @@ end ---@private ---@param bufnr? integer ----@return conform.FormatterUnit[] +---@return string[] M.list_formatters_for_buffer = function(bufnr) if not bufnr or bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() @@ -204,6 +254,10 @@ M.list_formatters_for_buffer = function(bufnr) local function dedupe_formatters(names, collect) for _, name in ipairs(names) do if type(name) == "table" then + notify_once( + "deprecated[conform]: The nested {} syntax to run the first formatter has been replaced by the stop_after_first option (see :help conform.format).\nSupport for the old syntax will be dropped on 2025-01-01.", + vim.log.levels.WARN + ) local alternation = {} dedupe_formatters(name, alternation) if not vim.tbl_isempty(alternation) then @@ -228,16 +282,6 @@ M.list_formatters_for_buffer = function(bufnr) if type(ft_formatters) == "function" then dedupe_formatters(ft_formatters(bufnr), formatters) else - -- support the old structure where formatters could be a subkey - if not islist(ft_formatters) then - vim.notify_once( - "Using deprecated structure for formatters_by_ft. See :help conform-options for details.", - vim.log.levels.ERROR - ) - ---@diagnostic disable-next-line: undefined-field - ft_formatters = ft_formatters.formatters - end - dedupe_formatters(ft_formatters, formatters) end end @@ -246,6 +290,25 @@ M.list_formatters_for_buffer = function(bufnr) return formatters end +---@param bufnr? integer +---@return nil|conform.DefaultFormatOpts +local function get_opts_from_filetype(bufnr) + if not bufnr or bufnr == 0 then + bufnr = vim.api.nvim_get_current_buf() + end + local matching_filetype = get_matching_filetype(bufnr) + if not matching_filetype then + return nil + end + + local ft_formatters = M.formatters_by_ft[matching_filetype] + assert(ft_formatters ~= nil, "get_matching_filetype ensures formatters_by_ft has key") + if type(ft_formatters) == "function" then + ft_formatters = ft_formatters(bufnr) + end + return merge_default_opts({}, ft_formatters) +end + ---@param bufnr integer ---@param mode "v"|"V" ---@return table {start={row,col}, end={row,col}} using (1, 0) indexing @@ -278,17 +341,18 @@ local function range_from_selection(bufnr, mode) end ---@private ----@param names conform.FormatterUnit[] +---@param names conform.FiletypeFormatterInternal ---@param bufnr integer ---@param warn_on_missing boolean +---@param stop_after_first boolean ---@return conform.FormatterInfo[] -M.resolve_formatters = function(names, bufnr, warn_on_missing) +M.resolve_formatters = function(names, bufnr, warn_on_missing, stop_after_first) local all_info = {} local function add_info(info, warn) if info.available then table.insert(all_info, info) elseif warn then - vim.notify( + notify( string.format("Formatter '%s' unavailable: %s", info.name, info.available_msg), vim.log.levels.WARN ) @@ -301,6 +365,10 @@ M.resolve_formatters = function(names, bufnr, warn_on_missing) local info = M.get_formatter_info(name, bufnr) add_info(info, warn_on_missing) else + notify_once( + "deprecated[conform]: The nested {} syntax to run the first formatter has been replaced by the stop_after_first option (see :help conform.format).\nSupport for the old syntax will be dropped on 2025-01-01.", + vim.log.levels.WARN + ) -- If this is an alternation, take the first one that's available for i, v in ipairs(name) do local info = M.get_formatter_info(v, bufnr) @@ -309,6 +377,10 @@ M.resolve_formatters = function(names, bufnr, warn_on_missing) end end end + + if stop_after_first and #all_info > 0 then + break + end end return all_info end @@ -328,34 +400,22 @@ local function has_lsp_formatter(opts) return not vim.tbl_isempty(lsp_format.get_format_clients(opts)) end ----@alias conform.LspFormatOpts ----| '"never"' # never use the LSP for formatting (default) ----| '"fallback"' # LSP formatting is used when no other formatters are available ----| '"prefer"' # use only LSP formatting when available ----| '"first"' # LSP formatting is used when available and then other formatters ----| '"last"' # other formatters are used then LSP formatting when available - ----@class conform.FormatOpts ----@field timeout_ms nil|integer Time in milliseconds to block for formatting. Defaults to 1000. No effect if async = true. ----@field bufnr nil|integer Format this buffer (default 0) ----@field async nil|boolean If true the method won't block. Defaults to false. If the buffer is modified before the formatter completes, the formatting will be discarded. ----@field dry_run nil|boolean If true don't apply formatting changes to the buffer ----@field undojoin nil|boolean Use undojoin to merge formatting changes with previous edit (default false) ----@field formatters nil|string[] List of formatters to run. Defaults to all formatters for the buffer filetype. ----@field lsp_format? conform.LspFormatOpts Configure if and when LSP should be used for formatting. Defaults to "never". ----@field quiet nil|boolean Don't show any notifications for warnings or failures. Defaults to false. ----@field 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 ----@field id nil|integer Passed to |vim.lsp.buf.format| when using LSP formatting ----@field name nil|string Passed to |vim.lsp.buf.format| when using LSP formatting ----@field filter nil|fun(client: table): boolean Passed to |vim.lsp.buf.format| when using LSP formatting +local has_notified_ft_no_formatters = {} ---Format a buffer ---@param opts? conform.FormatOpts ---@param callback? fun(err: nil|string, did_edit: nil|boolean) Called once formatting has completed ---@return boolean True if any formatters were attempted M.format = function(opts, callback) - ---@type {timeout_ms: integer, bufnr: integer, async: boolean, dry_run: boolean, lsp_format: "never"|"first"|"last"|"prefer"|"fallback", quiet: boolean, formatters?: string[], range?: conform.Range, undojoin: boolean} - opts = vim.tbl_extend("keep", opts or {}, { + opts = opts or {} + local has_explicit_formatters = opts.formatters ~= nil + -- If formatters were not passed in directly, fetch any options from formatters_by_ft + if not has_explicit_formatters then + merge_default_opts(opts, get_opts_from_filetype(opts.bufnr) or {}) + end + merge_default_opts(opts, M.default_format_opts) + ---@type {timeout_ms: integer, bufnr: integer, async: boolean, dry_run: boolean, lsp_format: "never"|"first"|"last"|"prefer"|"fallback", quiet: boolean, stop_after_first: boolean, formatters?: string[], range?: conform.Range, undojoin: boolean} + opts = vim.tbl_extend("keep", opts, { timeout_ms = 1000, bufnr = 0, async = false, @@ -363,7 +423,11 @@ M.format = function(opts, callback) lsp_format = "never", quiet = false, undojoin = false, + stop_after_first = false, }) + if opts.bufnr == 0 then + opts.bufnr = vim.api.nvim_get_current_buf() + end -- For backwards compatibility ---@diagnostic disable-next-line: undefined-field @@ -374,9 +438,6 @@ M.format = function(opts, callback) opts.lsp_format = "last" end - if opts.bufnr == 0 then - opts.bufnr = vim.api.nvim_get_current_buf() - end 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) @@ -387,18 +448,23 @@ M.format = function(opts, callback) local lsp_format = require("conform.lsp_format") local runner = require("conform.runner") - local explicit_formatters = opts.formatters ~= nil local formatter_names = opts.formatters or M.list_formatters_for_buffer(opts.bufnr) - local formatters = - M.resolve_formatters(formatter_names, opts.bufnr, not opts.quiet and explicit_formatters) + local formatters = M.resolve_formatters( + formatter_names, + opts.bufnr, + not opts.quiet and has_explicit_formatters, + opts.stop_after_first + ) local has_lsp = has_lsp_formatter(opts) + ---Handle errors and maybe run LSP formatting after cli formatters complete ---@param err? conform.Error ---@param did_edit? boolean local function handle_result(err, did_edit) if err then local level = errors.level_for_code(err.code) log.log(level, err.message) + ---@type boolean? local should_notify = not opts.quiet and level >= vim.log.levels.WARN -- Execution errors have special handling. Maybe should reconsider this. local notify_msg = err.message @@ -407,7 +473,7 @@ M.format = function(opts, callback) notify_msg = "Formatter failed. See :ConformInfo for details" end if should_notify then - vim.notify(notify_msg, level) + notify(notify_msg, level) end end local err_message = err and err.message @@ -427,6 +493,8 @@ M.format = function(opts, callback) callback(nil, did_edit) end end + + ---Run the resolved formatters on the buffer local function run_cli_formatters(cb) local resolved_names = vim.tbl_map(function(f) return f.name @@ -471,19 +539,25 @@ M.format = function(opts, callback) run_cli_formatters(handle_result) return true else - local level = explicit_formatters and "warn" or "debug" - log[level]("No formatters found for %s", vim.api.nvim_buf_get_name(opts.bufnr)) - callback("No formatters found for buffer") + local level = has_explicit_formatters and "warn" or "debug" + log[level]("Formatters unavailable for %s", vim.api.nvim_buf_get_name(opts.bufnr)) + + local ft = vim.bo[opts.bufnr].filetype + if + not vim.tbl_isempty(formatter_names) + and not has_notified_ft_no_formatters[ft] + and not opts.quiet + and M.notify_no_formatters + then + notify(string.format("Formatters unavailable for %s file", ft), vim.log.levels.WARN) + has_notified_ft_no_formatters[ft] = true + end + + callback("No formatters available for buffer") return false end end ----@class conform.FormatLinesOpts ----@field timeout_ms nil|integer Time in milliseconds to block for formatting. Defaults to 1000. No effect if async = true. ----@field bufnr nil|integer use this as the working buffer (default 0) ----@field async nil|boolean If true the method won't block. Defaults to false. If the buffer is modified before the formatter completes, the formatting will be discarded. ----@field quiet nil|boolean Don't show any notifications for warnings or failures. Defaults to false. - ---Process lines with formatters ---@private ---@param formatter_names string[] @@ -493,18 +567,20 @@ end ---@return nil|conform.Error error Only present if async = false ---@return nil|string[] new_lines Only present if async = false M.format_lines = function(formatter_names, lines, opts, callback) - ---@type {timeout_ms: integer, bufnr: integer, async: boolean, quiet: boolean} + ---@type {timeout_ms: integer, bufnr: integer, async: boolean, quiet: boolean, stop_after_first: boolean} opts = vim.tbl_extend("keep", opts or {}, { timeout_ms = 1000, bufnr = 0, async = false, quiet = false, + stop_after_first = false, }) callback = callback or function(_err, _lines) end local errors = require("conform.errors") local log = require("conform.log") local runner = require("conform.runner") - local formatters = M.resolve_formatters(formatter_names, opts.bufnr, not opts.quiet) + local formatters = + M.resolve_formatters(formatter_names, opts.bufnr, not opts.quiet, opts.stop_after_first) if vim.tbl_isempty(formatters) then callback(nil, lines) return @@ -540,7 +616,44 @@ M.list_formatters = function(bufnr) bufnr = vim.api.nvim_get_current_buf() end local formatters = M.list_formatters_for_buffer(bufnr) - return M.resolve_formatters(formatters, bufnr, false) + return M.resolve_formatters(formatters, bufnr, false, false) +end + +---Get the exact formatters that will be run for a buffer. +---@param bufnr? integer +---@return conform.FormatterInfo[] +---@return boolean lsp Will use LSP formatter +---@note +--- This accounts for stop_after_first, lsp fallback logic, etc. +M.list_formatters_to_run = function(bufnr) + if not bufnr or bufnr == 0 then + bufnr = vim.api.nvim_get_current_buf() + end + ---@type {bufnr: integer, lsp_format: conform.LspFormatOpts, stop_after_first: boolean} + local opts = vim.tbl_extend( + "keep", + get_opts_from_filetype(bufnr) or {}, + M.default_format_opts, + { stop_after_first = false, lsp_format = "never", bufnr = bufnr } + ) + local formatter_names = M.list_formatters_for_buffer(bufnr) + local formatters = M.resolve_formatters(formatter_names, bufnr, false, opts.stop_after_first) + + local has_lsp = has_lsp_formatter(opts) + local any_formatters = has_filetype_formatters(opts.bufnr) and not vim.tbl_isempty(formatters) + + if + has_lsp + and (opts.lsp_format == "prefer" or (opts.lsp_format ~= "never" and not any_formatters)) + then + return {}, true + elseif has_lsp and opts.lsp_format == "first" then + return formatters, true + elseif not vim.tbl_isempty(formatters) then + return formatters, opts.lsp_format == "last" and has_lsp + else + return {}, false + end end ---List information about all filetype-configured formatters @@ -551,18 +664,13 @@ M.list_all_formatters = function() if type(ft_formatters) == "function" then ft_formatters = ft_formatters(0) end - -- support the old structure where formatters could be a subkey - if not islist(ft_formatters) then - vim.notify_once( - "Using deprecated structure for formatters_by_ft. See :help conform-options for details.", - vim.log.levels.ERROR - ) - ---@diagnostic disable-next-line: undefined-field - ft_formatters = ft_formatters.formatters - end for _, formatter in ipairs(ft_formatters) do if type(formatter) == "table" then + notify_once( + "deprecated[conform]: The nested {} syntax to run the first formatter has been replaced by the stop_after_first option (see :help conform.format).\nSupport for the old syntax will be dropped on 2025-01-01.", + vim.log.levels.WARN + ) for _, v in ipairs(formatter) do formatters[v] = true end @@ -601,7 +709,7 @@ M.get_formatter_config = function(formatter, bufnr) if override and override.command and override.format then local msg = string.format("Formatter '%s' cannot define both 'command' and 'format' function", formatter) - vim.notify_once(msg, vim.log.levels.ERROR) + notify_once(msg, vim.log.levels.ERROR) return nil end @@ -623,7 +731,7 @@ M.get_formatter_config = function(formatter, bufnr) "Formatter '%s' missing built-in definition\nSet `command` to get rid of this error.", formatter ) - vim.notify_once(msg, vim.log.levels.ERROR) + notify_once(msg, vim.log.levels.ERROR) return nil end else @@ -707,9 +815,14 @@ M.get_formatter_info = function(formatter, bufnr) end ---Check if the buffer will use LSP formatting when lsp_format = "fallback" +---@deprecated ---@param options? table Options passed to |vim.lsp.buf.format| ---@return boolean M.will_fallback_lsp = function(options) + notify_once( + "deprecated[conform]: will_fallback_lsp is deprecated. Use list_formatters_to_run instead.\nThis method will be removed on 2025-01-01.", + vim.log.levels.WARN + ) options = vim.tbl_deep_extend("keep", options or {}, { bufnr = vim.api.nvim_get_current_buf(), }) diff --git a/lua/conform/types.lua b/lua/conform/types.lua index 05a4e4f..a4b8cc0 100644 --- a/lua/conform/types.lua +++ b/lua/conform/types.lua @@ -58,13 +58,53 @@ ---@field start integer[] ---@field end integer[] ----@alias conform.FormatterUnit string|string[] ----@alias conform.FiletypeFormatter conform.FormatterUnit[]|fun(bufnr: integer): string[] +---@alias conform.FiletypeFormatter conform.FiletypeFormatterInternal|fun(bufnr: integer): conform.FiletypeFormatterInternal + +---This list of formatters to run for a filetype, an any associated format options. +---@class conform.FiletypeFormatterInternal : conform.DefaultFormatOpts +---@field [integer] string + +---@alias conform.LspFormatOpts +---| '"never"' # never use the LSP for formatting (default) +---| '"fallback"' # LSP formatting is used when no other formatters are available +---| '"prefer"' # use only LSP formatting when available +---| '"first"' # LSP formatting is used when available and then other formatters +---| '"last"' # other formatters are used then LSP formatting when available + +---@class (exact) conform.FormatOpts +---@field timeout_ms? integer Time in milliseconds to block for formatting. Defaults to 1000. No effect if async = true. +---@field bufnr? integer Format this buffer (default 0) +---@field async? boolean If true the method won't block. Defaults to false. If the buffer is modified before the formatter completes, the formatting will be discarded. +---@field dry_run? boolean If true don't apply formatting changes to the buffer +---@field undojoin? boolean Use undojoin to merge formatting changes with previous edit (default false) +---@field formatters? string[] List of formatters to run. Defaults to all formatters for the buffer filetype. +---@field lsp_format? conform.LspFormatOpts Configure if and when LSP should be used for formatting. Defaults to "never". +---@field stop_after_first? boolean Only run the first available formatter in the list. Defaults to false. +---@field quiet? boolean Don't show any notifications for warnings or failures. Defaults to false. +---@field range? conform.Range 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 +---@field id? integer Passed to |vim.lsp.buf.format| when using LSP formatting +---@field name? string Passed to |vim.lsp.buf.format| when using LSP formatting +---@field filter? fun(client: table): boolean Passed to |vim.lsp.buf.format| when using LSP formatting + +---@class (exact) conform.DefaultFormatOpts +---@field timeout_ms? integer Time in milliseconds to block for formatting. Defaults to 1000. No effect if async = true. +---@field lsp_format? conform.LspFormatOpts Configure if and when LSP should be used for formatting. Defaults to "never". +---@field quiet? boolean Don't show any notifications for warnings or failures. Defaults to false. +---@field stop_after_first? boolean Only run the first available formatter in the list. Defaults to false. + +---@class conform.FormatLinesOpts +---@field timeout_ms? integer Time in milliseconds to block for formatting. Defaults to 1000. No effect if async = true. +---@field bufnr? integer use this as the working buffer (default 0) +---@field async? boolean If true the method won't block. Defaults to false. If the buffer is modified before the formatter completes, the formatting will be discarded. +---@field quiet? boolean Don't show any notifications for warnings or failures. Defaults to false. +---@field stop_after_first? boolean Only run the first available formatter in the list. Defaults to false. ---@class (exact) conform.setupOpts ---@field formatters_by_ft? table<string, conform.FiletypeFormatter> Map of filetype to formatters ---@field format_on_save? conform.FormatOpts|fun(bufnr: integer): nil|conform.FormatOpts If this is set, Conform will run the formatter on save. It will pass the table to conform.format(). This can also be a function that returns the table. +---@field default_format_opts? conform.DefaultFormatOpts The default options to use when calling conform.format() ---@field format_after_save? conform.FormatOpts|fun(bufnr: integer): nil|conform.FormatOpts If this is set, Conform will run the formatter asynchronously after save. It will pass the table to conform.format(). This can also be a function that returns the table. ---@field log_level? integer Set the log level (e.g. `vim.log.levels.DEBUG`). Use `:ConformInfo` to see the location of the log file. ---@field notify_on_error? boolean Conform will notify you when a formatter errors (default true). +---@field notify_no_formatters? boolean Conform will notify you when no formatters are available for the buffer (default true). ---@field formatters? table<string, conform.FormatterConfigOverride|fun(bufnr: integer): nil|conform.FormatterConfigOverride> Custom formatters and overrides for built-in formatters. diff --git a/scripts/options_doc.lua b/scripts/options_doc.lua index 193b462..561751d 100644 --- a/scripts/options_doc.lua +++ b/scripts/options_doc.lua @@ -4,8 +4,8 @@ require("conform").setup({ lua = { "stylua" }, -- Conform will run multiple formatters sequentially go = { "goimports", "gofmt" }, - -- Use a sub-list to run only the first available formatter - javascript = { { "prettierd", "prettier" } }, + -- You can also customize some of the format options for the filetype + rust = { "rustfmt", lsp_format = "fallback" }, -- You can use a function here to determine the formatters dynamically python = function(bufnr) if require("conform").get_formatter_info("ruff_format", bufnr).available then @@ -20,6 +20,11 @@ require("conform").setup({ -- have other formatters configured. ["_"] = { "trim_whitespace" }, }, + -- Set this to change the default values when calling conform.format() + -- This will also affect the default values for format_on_save/format_after_save + default_format_opts = { + lsp_format = "fallback", + }, -- If this is set, Conform will run the formatter on save. -- It will pass the table to conform.format(). -- This can also be a function that returns the table. @@ -38,6 +43,8 @@ require("conform").setup({ log_level = vim.log.levels.ERROR, -- Conform will notify you when a formatter errors notify_on_error = true, + -- Conform will notify you when no formatters are available for the buffer + notify_no_formatters = true, -- Custom formatters and overrides for built-in formatters formatters = { my_formatter = { @@ -74,7 +81,6 @@ require("conform").setup({ -- Set to false to disable merging the config with the base definition inherit = true, -- When inherit = true, add these additional arguments to the beginning of the command. - -- When inherit = true, add these additional arguments to the command. -- This can also be a function, like args prepend_args = { "--use-tabs" }, -- When inherit = true, add these additional arguments to the end of the command. diff --git a/tests/injected_spec.lua b/tests/injected_spec.lua index f6c7175..94d96b1 100644 --- a/tests/injected_spec.lua +++ b/tests/injected_spec.lua @@ -4,11 +4,6 @@ local injected = require("conform.formatters.injected") local runner = require("conform.runner") local test_util = require("tests.test_util") --- injected formatter only supported on neovim 0.9+ -if vim.fn.has("nvim-0.9") == 0 then - return -end - ---@param dir string ---@return string[] local function list_test_files(dir) |