🐥

neovim(nvim)のformatterをまあまあ調べた

2025/01/18に公開

たとえばこの手のjsxを編集しているとき

      <Head title="Welcome" />
      <div className="bg-gray-50 text-black/50 dark:bg-black dark:text-white/50">
        <img
          id="background"
          className="absolute -left-20 top-0 max-w-[877px]"
          src="https://laravel.com/assets/img/welcome/background.svg"
        />

oを押して編集しようとしたときに変な位置に入るのはインデントの設定がクソな状態なのではあるが、このような適当なインデントで保存した時でも正しく合わせてくれるのがformatterの仕事だ。(別に保存する事をトリガーにしなくてもいいが)

jsxでこれを行う一番ベタなツールがPrettierである

Prettierのinstallをnpm+mason-tool-installerでやる

~/.config/nvim/init.lua
-- lazy.nvim のセットアップ
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
  vim.fn.system({
    "git",
    "clone",
    "--filter=blob:none",
    "https://github.com/folke/lazy.nvim.git",
    "--branch=stable",
    lazypath,
  })
end
vim.opt.rtp:prepend(lazypath)

-- プラグイン設定
require("lazy").setup({

  -- mason.nvim 本体
  {
    "williamboman/mason.nvim",
    dependencies = {
      "WhoIsSethDaniel/mason-tool-installer.nvim",
    },
    config = function()
      require("mason").setup({
      })
    end,
  },
  -- mason-tool-installer
  {
    "WhoIsSethDaniel/mason-tool-installer.nvim",
    config = function()
      require("mason-tool-installer").setup({
        ensure_installed = {
          "prettier", -- Prettier を指定
        },
        auto_update = true, -- 自動更新を有効化
        run_on_start = true, -- Neovim 起動時にインストールを実行
      })
    end,
  },
})


システムにnpmのinstallが必須

$ ~/.local/share/nvim/mason/bin/prettier --version
3.4.2

フォーマッティングの仕掛け方には2種類ある

これは lspのプロトコル経由する 方法と、conform.nvimのようなフォーマット専用のプラグインを経由する方法の2つに大別されるようだ。とりあえずjsxを開いている場合だが、現在のファイルタイプを確認しておく。

:set filetype?

と入力すると

このように、ここではjavascriptreactが表示されている。

方法1: conform.nvimでformattingする

  -- conform.nvimのセットアップ
  {
    "stevearc/conform.nvim",
    config = function()
      require("conform").setup({
        formatters_by_ft = {
           javascriptreact = { "prettier" },
        },
      })

      -- 保存時に自動フォーマット
      vim.api.nvim_create_autocmd("BufWritePre", {
        callback = function(args)
           require("conform").format()
        end,
      })
    end,
  },

これを書き加える

保存してみる

高確率でこのようになるんじゃないかと思う。

Formatter failed. See :ConformInfo for details

に従い:ConformInfoしてみよう

ここでわかるのは

  • /home/admin/.local/share/nvim/mason/bin/prettier にprettierがインストールされている
  • ログは /home/admin/.local/state/nvim/conform.log にある

ということなので、それぞれ確認してみると

/home/admin/.local/share/nvim/mason/bin/prettier --version
3.4.2

これは起動できているが、

$ cat /home/admin/.local/state/nvim/conform.log
2025-01-17 08:55:59[ERROR] Formatter 'prettier' error: [error] /home/admin/inertiajs-v2-blog-react/resources/js/Pages/Welcome.jsx: Error: Cannot find package 'prettier-plugin-organize-imports' imported from /home/admin/inertiajs-v2-blog-react/noop.js
[error]     at new NodeError (file:///home/admin/.local/share/nvim/mason/packages/prettier/node_modules/prettier/index.mjs:15723:5)
[error]     at packageResolve (file:///home/admin/.local/share/nvim/mason/packages/prettier/node_modules/prettier/index.mjs:16664:9)
[error]     at moduleResolve (file:///home/admin/.local/share/nvim/mason/packages/prettier/node_modules/prettier/index.mjs:16704:18)
[error]     at defaultResolve (file:///home/admin/.local/share/nvim/mason/packages/prettier/node_modules/prettier/index.mjs:16795:16)
[error]     at resolve2 (file:///home/admin/.local/share/nvim/mason/packages/prettier/node_modules/prettier/index.mjs:16812:12)
[error]     at importFromFile (file:///home/admin/.local/share/nvim/mason/packages/prettier/node_modules/prettier/index.mjs:16827:16)
[error]     at importFromDirectory (file:///home/admin/.local/share/nvim/mason/packages/prettier/node_modules/prettier/index.mjs:21693:10)
[error]     at importPlugin (file:///home/admin/.local/share/nvim/mason/packages/prettier/node_modules/prettier/index.mjs:21705:12)
[error]     at async loadPluginWithoutCache (file:///home/admin/.local/share/nvim/mason/packages/prettier/node_modules/prettier/index.mjs:21709:18)
[error]     at async Promise.all (index 0)

prettier-plugin-organize-importsというプラグインがみつからないと言っている。この場合はプロジェクト側のnode_moduleにこれをinstallしてやるといいようだ

admin@ip-172-31-19-132:~/inertiajs-v2-blog-react$ npm install prettier-plugin-organize-imports
npm WARN deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm WARN deprecated @humanwhocodes/config-array@0.13.0: Use @eslint/config-array instead
npm WARN deprecated rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported
npm WARN deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
npm WARN deprecated @humanwhocodes/object-schema@2.0.3: Use @eslint/object-schema instead
npm WARN deprecated eslint@8.57.1: This version is no longer supported. Please see https://eslint.org/version-support for other options.

added 401 packages, and audited 402 packages in 8s

142 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

タイムアウトの対策

[WARN] Formatter 'prettier' timeout

このプラグインは、まあまあ遅いので

  -- conform.nvimのセットアップ
  {
    "stevearc/conform.nvim",
    config = function()
      require("conform").setup({
        formatters_by_ft = {
           javascriptreact = { "prettier" },
          -- typescriptreact = { "prettier" },
          -- lua = { "stylua" }, -- 必要に応じてLuaもフォーマット
          --typescript = { "prettier" },
        },
      })

      -- 保存時に自動フォーマット
      vim.api.nvim_create_autocmd("BufWritePre", {
        callback = function(args)
           -- 3000ms待つ
           require("conform").format({ bufnr = args.buf, timeout_ms = 3000 })
        end,
      })
    end,
  },

これくらい取らないとうまいこと動作しないようである。

bufnr = args.buf

は対象バッファのみを保存するとかいうオプションでまあ無条件に付けといた方が安全っぽい(多分

動作がクッソ遅い問題

上のアニgifでわかるか微妙なところだが、保存している間何もできていない。これはなかなかのストレスなので、これを何とかする

-           require("conform").format({ bufnr = args.buf, timeout_ms = 3000 })
+           require("conform").format({ bufnr = args.buf, timeout_ms = 3000, async = true })

このようにasync = trueを付けると、非同期でやってくれる。

ただ、このようにprettierの起動とかのタイミングとかの同期が取れてない時はうまいこと保存できてなかったりするので、まあまあキワドイ挙動だが無いよりは遥かにマシなのでセットしといてもいいんじゃないかという気はしますな。

方法2: lspのプロトコルを媒介し、null-lsで行う

  -- null-ls.nvim の設定
  {
    "jose-elias-alvarez/null-ls.nvim",
    dependencies = {
      "nvim-lua/plenary.nvim", -- null-ls の依存関係
      "williamboman/mason.nvim", -- Mason と統合するために必要
      "jayp0521/mason-null-ls.nvim", -- Mason と null-ls の統合
    },
    config = function()
      local null_ls = require("null-ls")
      require("mason-null-ls").setup({
        ensure_installed = { "prettier" }, -- null-ls 経由で Prettier をインストール
        automatic_installation = true,
      })

      -- null-ls のソース設定
      null_ls.setup({
        sources = {
          null_ls.builtins.formatting.prettier.with({
            filetypes = { "javascriptreact" }, -- 必要なファイルタイプを指定
          }),
        },
      })

      -- 保存時に自動フォーマット
      vim.api.nvim_create_autocmd("BufWritePre", {
        callback = function(args)
          vim.lsp.buf.format({ bufnr = args.buf, timeout_ms = 3000, async = true })
        end,
      })
    end,
  },

こうする事により、LSPを通じたフォーマッティングになるようだ。結果としては同じだが経緯は大分違う事になってそう。

ここでも

vim.lsp.buf.format({ bufnr = args.buf, timeout_ms = 3000, async = true })

このようにする事により、タイムアウトと非同期をconformと同一にしている。従って操作感はほとんど差異が無い。


null-lsを経由した操作

ただし、null-lsは今現在、none-lsがalternativeとして提示されているので

https://github.com/nvimtools/none-ls.nvim

したがってここにあるように

  -- none-ls.nvim の設定
  {
    "nvimtools/none-ls.nvim",

とした方がいいんだろうと思う

調子に乗ってluaもformattingする

なんだかんだnvimをキメてるとどうやったってluaを書くはめになる。noconfigだとこれもちゃんとindentがキマってないので結構ややこしい感じになっている

まあインデントの問題は後から考えるとして保存する時にformatされてればいいのでって事で

styluaとlua-language-server

  -- mason-tool-installer
  {
    "WhoIsSethDaniel/mason-tool-installer.nvim",
    config = function()
      require("mason-tool-installer").setup({
        ensure_installed = {
          "prettier",
          "stylua",
          "lua-language-server",
        },
        auto_update = true,
        run_on_start = true,
      })
    end,
  },

とする。

stylua: failed to install

とかなった場合は

:MasonLog

すると

このようにいろいろ原因が掴めるかもしれない


unzipを入れる事で導入に成功したメリ

$ /home/admin/.local/share/nvim/mason/bin/stylua --version
stylua 2.0.2

設定

  -- none-ls.nvim の設定
  {
    "nvimtools/none-ls.nvim",
    dependencies = {
      "nvim-lua/plenary.nvim", -- none-ls の依存関係
      "williamboman/mason.nvim", -- Mason と統合するために必要
      "jayp0521/mason-null-ls.nvim", -- Mason と none-ls の統合
    },
    config = function()
      local null_ls = require("null-ls")
      require("mason-null-ls").setup({
        ensure_installed = { "prettier", "stylua" },
        automatic_installation = true,
      })

      -- null-ls のソース設定
      null_ls.setup({
        sources = {
          null_ls.builtins.formatting.prettier.with({
            filetypes = { "javascriptreact" },
          }),
          null_ls.builtins.formatting.stylua.with({
                  filetypes = { "lua" }, -- Lua ファイルに適用
          }),
        },
      })

      -- 保存時に自動フォーマット
      vim.api.nvim_create_autocmd("BufWritePre", {
        callback = function(args)
          vim.lsp.buf.format({ bufnr = args.buf, timeout_ms = 3000, async = true })
        end,
      })
    end,
  },

こんな感じにすると...

こんな事になるので

~/.stylua.toml
column_width = 80
indent_type = "Spaces"
indent_width = 2
quote_style = "AutoPreferSingle"

などしておくとよさそうである

これももちろんconformでも設定できるだろうから、やってみるのもよいと思う(筆者はここで力尽きた)

使えるformatter (conform.nvimで)

https://github.com/stevearc/conform.nvim?tab=readme-ov-file#formatters

ここにリストがある。俺たちのw phpにいたっては適当にAIで翻訳した限り

  • easy-coding-standard (ecs) - PHP-CS-Fixer や PHP_CodeSniffer の知識ゼロで、コーディングスタンダードを使用できるツール。
  • php_cs_fixer - PHP コーディングスタンダードを適用するための修正ツール。
  • phpcbf - PHP Code Beautifier and Fixer は、定義されたコーディングスタンダードの違反を修正するツール。
  • phpinsights - PHP プロジェクトのコード品質を分析するための完璧なスタート地点。
  • pint - Laravel Pint は、ミニマリスト向けの意見に基づいた PHP コードスタイル修正ツール。
  • pretty-php - 意見に基づいた PHP コードフォーマッター。

こんなにもあるようなので、そのうちネタにして行こうかなとおもうメリ

今回設定したlua

formattingされてるぞい

~/.config/nvim/init.lua
-- lazy.nvim のセットアップ
local lazypath = vim.fn.stdpath('data') .. '/lazy/lazy.nvim'
if not vim.loop.fs_stat(lazypath) then
  vim.fn.system({
    'git',
    'clone',
    '--filter=blob:none',
    'https://github.com/folke/lazy.nvim.git',
    '--branch=stable',
    lazypath,
  })
end
vim.opt.rtp:prepend(lazypath)

-- プラグイン設定
require('lazy').setup({
  -- mason.nvim 本体
  {
    'williamboman/mason.nvim',
    dependencies = {
      'WhoIsSethDaniel/mason-tool-installer.nvim',
    },
    config = function()
      require('mason').setup({})
    end,
  },

  -- mason-tool-installer
  {
    'WhoIsSethDaniel/mason-tool-installer.nvim',
    config = function()
      require('mason-tool-installer').setup({
        ensure_installed = {
          'prettier',
          'stylua',
        },
        auto_update = true,
        run_on_start = true,
      })
    end,
  },

  -- none-ls.nvim の設定
  {
    'nvimtools/none-ls.nvim',
    dependencies = {
      'nvim-lua/plenary.nvim', -- none-ls の依存関係
      'williamboman/mason.nvim', -- Mason と統合するために必要
      'jayp0521/mason-null-ls.nvim', -- Mason と none-ls の統合
    },
    config = function()
      local null_ls = require('null-ls')
      require('mason-null-ls').setup({
        ensure_installed = { 'prettier', 'stylua' },
        automatic_installation = true,
      })

      -- null-ls のソース設定
      null_ls.setup({
        sources = {
          null_ls.builtins.formatting.prettier.with({
            filetypes = { 'javascriptreact' },
          }),
          null_ls.builtins.formatting.stylua.with({
            filetypes = { 'lua' }, -- Lua ファイルに適用
          }),
        },
      })

      -- 保存時に自動フォーマット
      vim.api.nvim_create_autocmd('BufWritePre', {
        callback = function(args)
          vim.lsp.buf.format({
            bufnr = args.buf,
            timeout_ms = 3000,
            async = true,
          })
        end,
      })
    end,
  },
})

--  -- conform.nvimのセットアップ
--  {
--    "stevearc/conform.nvim",
--    config = function()
--      require("conform").setup({
--        formatters_by_ft = {
--           javascriptreact = { "prettier" },
--          -- typescriptreact = { "prettier" },
--          -- lua = { "stylua" }, -- 必要に応じてLuaもフォーマット
--          --typescript = { "prettier" },
--        },
--      })
--
--      -- 保存時に自動フォーマット
--      vim.api.nvim_create_autocmd("BufWritePre", {
--        callback = function(args)
--           require("conform").format({ bufnr = args.buf, timeout_ms = 3000, async = true })
--        end,
--      })
--    end,
--  },
--})

Discussion