diff --git a/plugins/lsp/conform-nvim.nix b/plugins/lsp/conform-nvim.nix index 89096463..5760589c 100644 --- a/plugins/lsp/conform-nvim.nix +++ b/plugins/lsp/conform-nvim.nix @@ -1,24 +1,100 @@ { lib, - helpers, config, pkgs, ... }: -with lib; let - cfg = config.plugins.conform-nvim; + inherit (lib) types; + inherit (lib.nixvim) defaultNullOpts mkRaw; in -{ - options.plugins.conform-nvim = helpers.neovim-plugin.extraOptionsOptions // { - enable = mkEnableOption "conform-nvim"; +lib.nixvim.neovim-plugin.mkNeovimPlugin config { + name = "conform-nvim"; + luaName = "conform"; + originalName = "conform.nvim"; + defaultPackage = pkgs.vimPlugins.conform-nvim; - package = helpers.mkPluginPackageOption "conform-nvim" pkgs.vimPlugins.conform-nvim; + maintainers = [ lib.maintainers.khaneliman ]; + + # TODO: added 2024-08-23 remove after 24.11 + deprecateExtraOptions = true; + optionsRenamedToSettings = [ + "formatters" + "formattersByFt" + "logLevel" + "notifyOnError" + ]; + imports = + map + ( + optionName: + lib.mkRemovedOptionModule + [ + "plugins" + "conform-nvim" + optionName + ] + '' + Please use `plugins.conform-nvim.settings.${lib.nixvim.toSnakeCase optionName}` instead. + + Note that nested options will now be snake_case, as well, to match upstream plugin configuration. + '' + ) + [ + "formatAfterSave" + "formatOnSave" + ]; + + settingsOptions = + let + lsp_format = + defaultNullOpts.mkEnumFirstDefault + [ + "never" + "fallback" + "prefer" + "first" + "last" + ] + '' + Option for choosing lsp formatting preference. + + - `never`: never use the LSP for formatting. + - `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. + ''; + + timeout_ms = defaultNullOpts.mkUnsignedInt 1000 "Time in milliseconds to block for formatting."; + + quiet = defaultNullOpts.mkBool false "Don't show any notifications for warnings or failures."; + + stop_after_first = defaultNullOpts.mkBool false "Only run the first available formatter in the list."; + + # NOTE: These are the available options for the `default_format_opts` opt. + # They are also included in the `format_opts` options available to `format_after_save` and `format_on_save`. + defaultFormatSubmodule = + with types; + (submodule { + freeformType = attrsOf anything; + options = { + inherit + lsp_format + timeout_ms + quiet + stop_after_first + ; + }; + }); + in + { + formatters_by_ft = defaultNullOpts.mkAttrsOf types.anything { } '' + Creates a table mapping filetypes to formatters. + + You can run multiple formatters in a row by adding them in a list. + If you'd like to stop after the first successful formatter, set `stop_after_first`. - formattersByFt = helpers.defaultNullOpts.mkAttrsOf' { - type = types.anything; - # Unknown plugin default - description = '' ```nix # Map of filetype to formatters formattersByFt = @@ -26,8 +102,12 @@ in 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" ] ]; + # Use stop_after_first to run only the first available formatter + javascript = { + __unkeyed-1 = "prettierd"; + __unkeyed-2 = "prettier"; + stop_after_first = true; + }; # Use the "*" filetype to run formatters on all filetypes. "*" = [ "codespell" ]; # Use the "_" filetype to run formatters on filetypes that don't @@ -36,97 +116,126 @@ in }; ``` ''; + + format_on_save = defaultNullOpts.mkNullable' { + type = with types; either strLuaFn defaultFormatSubmodule; + pluginDefault = { }; + description = '' + If this is set, Conform will run the formatter asynchronously on save. + + Conform will pass the table from `format_on_save` to `conform.format()`. + This can also be a function that returns the table. + + See `:help conform.format` for details. + ''; + apply = v: if lib.isString v then mkRaw v else v; + }; + + default_format_opts = defaultNullOpts.mkNullable defaultFormatSubmodule { } '' + The default options to use when calling `conform.format()`. + ''; + + format_after_save = defaultNullOpts.mkNullable' { + type = with types; either strLuaFn defaultFormatSubmodule; + pluginDefault = { }; + description = '' + If this is set, Conform will run the formatter asynchronously after save. + + Conform will pass the table from `format_after_save` to `conform.format()`. + This can also be a function that returns the table. + + See `:help conform.format` for details. + ''; + apply = v: if lib.isString v then mkRaw v else v; + }; + + log_level = defaultNullOpts.mkLogLevel "error" '' + Set the log level. Use `:ConformInfo` to see the location of the log file. + ''; + + notify_on_error = defaultNullOpts.mkBool true "Conform will notify you when a formatter errors."; + + notify_no_formatters = defaultNullOpts.mkBool true "Conform will notify you when no formatters are available for the buffer."; + + formatters = + defaultNullOpts.mkAttrsOf types.anything { } + "Custom formatters and changes to built-in formatters."; }; - formatOnSave = helpers.defaultNullOpts.mkNullable' { - type = - with helpers.nixvimTypes; - either strLuaFn (submodule { - options = { - lspFallback = mkOption { - type = types.bool; - default = true; - description = "See :help conform.format for details."; - }; - timeoutMs = mkOption { - type = types.int; - default = 500; - description = "See :help conform.format for details."; - }; + settingsExample = lib.literalMD '' + ```nix + { + formattersByFt = { + bash = [ + "shellcheck" + "shellharden" + "shfmt" + ]; + cpp = [ "clang_format" ]; + javascript = { + __unkeyed-1 = "prettierd"; + __unkeyed-2 = "prettier"; + timeout_ms = 2000; + stop_after_first = true; }; - }); - # Unknown plugin default - description = '' - 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. - See :help conform.format for details. - ''; - }; + "_" = [ + "squeeze_blanks" + "trim_whitespace" + "trim_newlines" + ]; + }; + format_on_save = # Lua + ''' + function(bufnr) + if vim.g.disable_autoformat or vim.b[bufnr].disable_autoformat then + return + end - formatAfterSave = helpers.defaultNullOpts.mkNullable' { - type = - with helpers.nixvimTypes; - either strLuaFn (submodule { - options = { - lspFallback = mkOption { - type = types.bool; - default = true; - description = "See :help conform.format for details."; - }; + if slow_format_filetypes[vim.bo[bufnr].filetype] then + return + end + + local function on_format(err) + if err and err:match("timeout$") then + slow_format_filetypes[vim.bo[bufnr].filetype] = true + end + end + + return { timeout_ms = 200, lsp_fallback = true }, on_format + end + '''; + format_after_save = # Lua + ''' + function(bufnr) + if vim.g.disable_autoformat or vim.b[bufnr].disable_autoformat then + return + end + + if not slow_format_filetypes[vim.bo[bufnr].filetype] then + return + end + + return { lsp_fallback = true } + end + '''; + log_level = "warn"; + notify_on_error = false; + notify_no_formatters = false; + formatters = { + shellcheck = { + command = lib.getExe pkgs.shellcheck; }; - }); - # Unknown plugin default - description = '' - 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. - ''; - }; - - logLevel = helpers.defaultNullOpts.mkLogLevel "error" '' - Set the log level. Use `:ConformInfo` to see the location of the log file. - ''; - - notifyOnError = helpers.defaultNullOpts.mkBool true "Conform will notify you when a formatter errors"; - - formatters = helpers.defaultNullOpts.mkAttrsOf' { - type = types.anything; - # Unknown plugin default - description = "Custom formatters and changes to built-in formatters"; - }; - }; - - config = - let - setupOptions = - with cfg; - { - formatters_by_ft = formattersByFt; - format_on_save = - if builtins.isAttrs formatOnSave then - { - lsp_fallback = formatOnSave.lspFallback; - timeout_ms = formatOnSave.timeoutMs; - } - else - helpers.mkRaw formatOnSave; - format_after_save = - if builtins.isAttrs formatAfterSave then - { lsp_fallback = formatOnSave.lspFallback; } - else - helpers.mkRaw formatAfterSave; - log_level = logLevel; - notify_on_error = notifyOnError; - inherit formatters; - } - // cfg.extraOptions; - in - mkIf cfg.enable { - extraPlugins = [ cfg.package ]; - - extraConfigLua = '' - require("conform").setup(${helpers.toLuaObject setupOptions}) - ''; - }; + shfmt = { + command = lib.getExe pkgs.shfmt; + }; + shellharden = { + command = lib.getExe pkgs.shellharden; + }; + squeeze_blanks = { + command = lib.getExe' pkgs.coreutils "cat"; + }; + }; + } + ``` + ''; } diff --git a/tests/test-sources/plugins/lsp/conform-nvim.nix b/tests/test-sources/plugins/lsp/conform-nvim.nix index 79af0593..9a640b9c 100644 --- a/tests/test-sources/plugins/lsp/conform-nvim.nix +++ b/tests/test-sources/plugins/lsp/conform-nvim.nix @@ -1,3 +1,4 @@ +{ lib, pkgs, ... }: { empty = { plugins.conform-nvim.enable = true; @@ -7,69 +8,107 @@ plugins.conform-nvim = { enable = true; - formattersByFt = { - lua = [ "stylua" ]; - python = [ - "isort" - "black" - ]; - javascript = [ - [ - "prettierd" - "prettier" - ] - ]; - "*" = [ "codespell" ]; - "_" = [ "trimWhitespace" ]; - }; - formatOnSave = { - lspFallback = true; - timeoutMs = 500; - }; - formatAfterSave = { - lspFallback = true; - }; - logLevel = "error"; - notifyOnError = true; - formatters = { - myFormatter = { - command = "myCmd"; - args = [ - "--stdin-from-filename" - "$FILENAME" - ]; - rangeArgs = '' - function(ctx) - return { "--line-start", ctx.range.start[1], "--line-end", ctx.range["end"][1] } - end; - ''; - stdin = true; - cwd = '' - require("conform.util").rootFile({ ".editorconfig", "package.json" }); - ''; - requireCwd = true; - condition = '' - function(ctx) - return vim.fs.basename(ctx.filename) ~= "README.md" - end; - ''; - exitCodes = [ - 0 - 1 - ]; - env = { - VAR = "value"; - }; - "inherit" = true; - prependArgs = [ "--use-tabs" ]; + settings = { + formatters_by_ft = { }; + format_on_save = { + lsp_format = "never"; + timeout_ms = 1000; + quiet = false; + stop_after_first = false; + }; + default_format_opts = { + lsp_format = "never"; + timeout_ms = 1000; + quiet = false; + stop_after_first = false; + }; + format_after_save = { + lsp_format = "never"; + timeout_ms = 1000; + quiet = false; + stop_after_first = false; + }; + log_level = "error"; + notify_on_error = true; + notify_no_formatters = true; + formatters = { }; + }; + }; + }; + + example = { + plugins.conform-nvim = { + enable = true; + + settings = { + formatters_by_ft = { + lua = [ "stylua" ]; + python = [ + "isort" + "black" + ]; + javascript = { + __unkeyed-1 = "prettierd"; + __unkeyed-2 = "prettier"; + timeout_ms = 2000; + stop_after_first = true; + }; + "*" = [ "codespell" ]; + "_" = [ "trimWhitespace" ]; + }; + format_on_save = { + lsp_format = "fallback"; + timeout_ms = 500; + }; + format_after_save = { + lsp_format = "fallback"; + }; + log_level = "error"; + notify_on_error = false; + notify_no_formatters = false; + formatters = { + nixfmt = { + command = lib.getExe pkgs.nixfmt-rfc-style; + }; + myFormatter = { + command = "myCmd"; + args = [ + "--stdin-from-filename" + "$FILENAME" + ]; + rangeArgs = '' + function(ctx) + return { "--line-start", ctx.range.start[1], "--line-end", ctx.range["end"][1] } + end; + ''; + stdin = true; + cwd = '' + require("conform.util").rootFile({ ".editorconfig", "package.json" }); + ''; + requireCwd = true; + condition = '' + function(ctx) + return vim.fs.basename(ctx.filename) ~= "README.md" + end; + ''; + exitCodes = [ + 0 + 1 + ]; + env = { + VAR = "value"; + }; + "inherit" = true; + prependArgs = [ "--use-tabs" ]; + }; + otherFormatter = '' + function(bufnr) + return { + command = "myCmd"; + } + end; + ''; }; - otherFormatter = '' - function(bufnr) - return { - command = "myCmd"; - } - end; - ''; }; }; }; @@ -78,42 +117,28 @@ plugins.conform-nvim = { enable = true; - formattersByFt = { - lua = [ "stylua" ]; - python = [ - "isort" - "black" - ]; - javascript = [ - [ - "prettierd" - "prettier" - ] - ]; - "*" = [ "codespell" ]; - "_" = [ "trimWhitespace" ]; + settings = { + format_on_save = '' + function(bufnr) + local ignore_filetypes = { "helm" } + if vim.tbl_contains(ignore_filetypes, vim.bo[bufnr].filetype) then + return + end + + -- Disable with a global or buffer-local variable + if vim.g.disable_autoformat or vim.b[bufnr].disable_autoformat then + return + end + + -- Disable autoformat for files in a certain path + local bufname = vim.api.nvim_buf_get_name(bufnr) + if bufname:match("/node_modules/") then + return + end + return { timeout_ms = 500, lsp_fallback = true } + end + ''; }; - - formatOnSave = '' - function(bufnr) - local ignore_filetypes = { "helm" } - if vim.tbl_contains(ignore_filetypes, vim.bo[bufnr].filetype) then - return - end - - -- Disable with a global or buffer-local variable - if vim.g.disable_autoformat or vim.b[bufnr].disable_autoformat then - return - end - - -- Disable autoformat for files in a certain path - local bufname = vim.api.nvim_buf_get_name(bufnr) - if bufname:match("/node_modules/") then - return - end - return { timeout_ms = 500, lsp_fallback = true } - end - ''; }; }; @@ -121,33 +146,19 @@ plugins.conform-nvim = { enable = true; - formattersByFt = { - lua = [ "stylua" ]; - python = [ - "isort" - "black" - ]; - javascript = [ - [ - "prettierd" - "prettier" - ] - ]; - "*" = [ "codespell" ]; - "_" = [ "trimWhitespace" ]; + settings = { + format_after_save = '' + function(bufnr) + if vim.g.disable_autoformat or vim.b[bufnr].disable_autoformat then + return + end + if not _conform_slow_format_filetypes[vim.bo[bufnr].filetype] then + return + end + return { lsp_fallback = true } + end + ''; }; - - formatAfterSave = '' - function(bufnr) - if vim.g.disable_autoformat or vim.b[bufnr].disable_autoformat then - return - end - if not _conform_slow_format_filetypes[vim.bo[bufnr].filetype] then - return - end - return { lsp_fallback = true } - end - ''; }; }; }