🐼

nvim-lspの作用に少し凝ったカスタマイズを加える旅

2024/04/10に公開

はじめに

nvim-lspはデフォルトでかなり使いやすいですが、もう少し自分の使い方にあったカスタマズを加えたいと思うことはあります。
今回、これを設定するための方法を探してみたら、思いのほか長い旅の割に簡単な設定で片付いたので、ここに記録しておきます。

やりたかったこと

nvim-lspのデフォルトの設定では、signcolumnに表示されるエラー/警告/ヒントの位置が重なると、
エラー/警告/ヒントのどれが表示されるかは予測するのが難しくなっています。
しかし、エラー/警告/ヒントの表示順を深刻度の高い方から表示したいものです。

結論としての設定

今回のやりたかったことは、次のような設定で片が付きました。

vim.diagnostic.config({severity_sort = true})

シンプルですね。

模索の旅

この設定に辿り着くまでにヘルプを読みあさったので、その課程を記録しておきます。

signの表示方法を変える設定を探す

まずは、nvim-lspの設定でsignを変える方法を探してみます。

  1. :help lsp でLSP関連のヘルプを開く
  2. /sign でsigncolumnに関する項目を探す

なにやら、:help lsp-handler-configurationにぶち当たります。

                                                   *lsp-handler-configuration*

To configure the behavior of a builtin |lsp-handler|, the convenient method
|vim.lsp.with()| is provided for users.

  To configure the behavior of |vim.lsp.diagnostic.on_publish_diagnostics()|,
  consider the following example, where a new |lsp-handler| is created using
  |vim.lsp.with()| that no longer generates signs for the diagnostics: >lua

    vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with(
      vim.lsp.diagnostic.on_publish_diagnostics, {
        -- Disable signs
        signs = false,
      }
    )

要は、nvim-lspはvim.lsp.handlersによって、LSからの情報を受け取り、ハンドリングしているようです。
signに関する情報は(signatureという紛らわしい情報がりますが)これ以外に周辺情報はないようです。

vim.lsp.with自体は、vim.lsp.diagnostic.on_publish_diagnosticsをラップして、新しいハンドラを作成する関数のようです。

/usr/local/share/nvim/runtime/lua/vim/lsp.lua
--- Function to manage overriding defaults for LSP handlers.
---@param handler (lsp.Handler) See |lsp-handler|
---@param override_config (table) Table containing the keys to override behavior of the {handler}
function lsp.with(handler, override_config)
  return function(err, result, ctx, config)
    return handler(err, result, ctx, vim.tbl_deep_extend('force', config or {}, override_config))
  end
end

vim.lsp.handlersについて調べる

次にvim.lsp.handlersに関する情報を探してみます。
vim.lsp.handlersには、LSのメソッドに対して、どのような処理を行うか(ハンドラ)が管理されています。
各ハンドラは、:help lsp-handlerによると、次のようなシグネチャを持っています。

                                                                 *lsp-handler*
LSP handlers are functions that handle |lsp-response|s to requests made by Nvim
to the server. (Notifications, as opposed to requests, are fire-and-forget:
there is no response, so they can't be handled. |lsp-notification|)

Each response handler has this signature: >

    function(err, result, ctx, config)
<
    Parameters: ~
        - {err}     (table|nil) Error info dict, or `nil` if the request
                    completed.
        - {result}  (Result | Params | nil) `result` key of the |lsp-response| or
                    `nil` if the request failed.
        - {ctx}     (table) Table of calling state associated with the
                    handler, with these keys:
                    - {method}  (string) |lsp-method| name.
                    - {client_id} (number) |vim.lsp.Client| identifier.
                    - {bufnr}   (Buffer) Buffer handle.
                    - {params}  (table|nil) Request parameters table.
                    - {version} (number) Document version at time of
                                request. Handlers can compare this to the
                                current document version to check if the
                                response is "stale". See also |b:changedtick|.
        - {config}  (table) Handler-defined configuration table, which allows
                    users to customize handler behavior.
                    For an example, see:
                        |vim.lsp.diagnostic.on_publish_diagnostics()|
                    To configure a particular |lsp-handler|, see:
                        |lsp-handler-configuration|

    Returns: ~
        Two values `result, err` where `err` is shaped like an RPC error: >
            { code, message, data? }
<        You can use |vim.lsp.rpc.rpc_response_error()| to create this object.

そして、前述の:help lsp-handler-configurationに書かれた内容から、signを表示しているハンドラがわかります。
vim.lsp.diagnostic.on_publish_diagnostics()ですね。

ちなみに、今回はたまたま検索で引っかかりましたが、各LSのメソッドに対するハンドラは、:help lsp-handlersにまとめられています。

Lua module: vim.lsp.handlers                                    *lsp-handlers*

hover({_}, {result}, {ctx}, {config})               *vim.lsp.handlers.hover()*
    |lsp-handler| for the method "textDocument/hover"

(長いので以下略)
各ハンドラに、どのメソッドが対応しているか、どのような設定が可能かが記載されています。

ハンドラの設定内容について調べる

今回のハンドラに渡せるものは何かを確認してみます。

                                 *vim.lsp.diagnostic.on_publish_diagnostics()*
on_publish_diagnostics({_}, {result}, {ctx}, {config})
    |lsp-handler| for the method "textDocument/publishDiagnostics"

    See |vim.diagnostic.config()| for configuration options. Handler-specific
    configuration can be set using |vim.lsp.with()|: >lua
config({opts}, {namespace})                          *vim.diagnostic.config()*
    Configure diagnostic options globally or for a specific diagnostic
    namespace.

    Configuration can be specified globally, per-namespace, or ephemerally
    (i.e. only for a single call to |vim.diagnostic.set()| or
    |vim.diagnostic.show()|). Ephemeral configuration has highest priority,
    followed by namespace configuration, and finally global configuration.

    For example, if a user enables virtual text globally with >lua
        vim.diagnostic.config({ virtual_text = true })
<

    and a diagnostic producer sets diagnostics with >lua
        vim.diagnostic.set(ns, 0, diagnostics, { virtual_text = false })
<

    then virtual text will not be enabled for those diagnostics.

    Parameters: ~
      • {opts}       (`vim.diagnostic.Opts?`) When omitted or `nil`, retrieve
                     the current configuration. Otherwise, a configuration
                     table (see |vim.diagnostic.Opts|).
*vim.diagnostic.Opts*
    Each of the configuration options below accepts one of the following:
    • `false`: Disable this feature
    • `true`: Enable this feature, use default settings.
    • `table`: Enable this feature with overrides. Use an empty table to use
      default values.
    • `function`: Function with signature (namespace, bufnr) that returns any
      of the above.

    Fields: ~

    ... 中略 ...

      • {signs}?             (`boolean|vim.diagnostic.Opts.Signs|fun(namespace: integer, bufnr:integer): vim.diagnostic.Opts.Signs`, default: `true`)
                             Use signs for diagnostics |diagnostic-signs|.

なかなかたらい回しになりましたが、diagnotic-signsなる機構を使っているようです。

diagnostic-signsについて調べる

なんじゃらほい、と思いつつ、:help diagnostic-signsを見てみます。

SIGNS                                                   *diagnostic-signs*

Signs are defined for each diagnostic severity. The default text for each sign
is the first letter of the severity name (for example, "E" for ERROR). Signs
can be customized with |vim.diagnostic.config()|. Example: >lua

    -- Highlight entire line for errors
    -- Highlight the line number for warnings
    vim.diagnostic.config({
        signs = {
            text = {
                [vim.diagnostic.severity.ERROR] = '',
                [vim.diagnostic.severity.WARN] = '',
            },
            linehl = {
                [vim.diagnostic.severity.ERROR] = 'ErrorMsg',
            },
            numhl = {
                [vim.diagnostic.severity.WARN] = 'WarningMsg',
            },
        },
    })

When the "severity_sort" option is set (see |vim.diagnostic.config()|) the
priority of each sign depends on the severity of the associated diagnostic.
Otherwise, all signs have the same priority (the value of the "priority"
option in the "signs" table of |vim.diagnostic.config()| or 10 if unset).

Neovimではlspの諸々とdiagnosticsの設定は独立しているとは聞いていましたが、ここに来て衝撃的なことが書かれています。

When the "severity_sort" option is set (see |vim.diagnostic.config()|) the
priority of each sign depends on the severity of the associated diagnostic.
Otherwise, all signs have the same priority (the value of the "priority"
option in the "signs" table of |vim.diagnostic.config()| or 10 if unset).

そう、vim.diagnostic.config()severity_sortなるFieldを設定すれば、signの表示優先度を変更できるようです。

      • {severity_sort}?     (`boolean|{reverse?:boolean}`, default: `false)
                             Sort diagnostics by severity. This affects the
                             order in which signs and virtual text are
                             displayed. When true, higher severities are
                             displayed before lower severities (e.g. ERROR is
                             displayed before WARN). Options:
                             • {reverse}? (boolean) Reverse sort order

結論としての設定(再掲)

今回のやりたかったことは、次のような設定で片が付きました。

vim.diagnostic.config({severity_sort = true})

Discussion