mizdra's blog

ぽよぐらみんぐ

@.css-modules-kit/ts-plugin を Neovim で動かすまでの覚書

id:mizdraCSS Modules Kit という CSS Modules のためのツールキットを作っている。そのツールキットの中に @css-modules-kit/ts-plugin というものがある。CSS Modules の言語機能を提供するための TypeScript Language Service Plugin である。

一応これは任意のエディタで動かせるように作ってる。実際、VS Code / Zed などで動くことを確認してる。しかし、Neovim で動くことはまだ確認できてなかった。いや、動くはずなんだけど Neovim の使い方がさっぱりわからなくて... 何度か挑戦したけど、LSP の設定の仕方とか、plugin のインストールとかが難しくて挫折した。あとキーバインドが全然わからん。completion のウインドウってどうやって出すの? そもそも TypeScript Language Service Plugin が読み込まれているか確認するために tsserver.log を見たいのだけど、どうやったら見れるの?

...という状態だったのだけど、この度 Claude に色々聞きながらようやく @css-modules-kit/ts-plugin を動かすことができた。忘れないように覚えたことを記事にまとめておく。

Neovim のインストール

macOS なら Homebrew でインストールできる。

インストールしたら nvim で起動できるはず。

設定ファイルの置き場所

OS によって設定ファイルの置き場所は異なるが、macOS なら ~/.config/nvim$XDG_CONFIG_HOME/nvim になる。$XDG_CONFIG_HOME がセットされてない時は前者が、セットされてる時は後者が使われる。

ここに init.viminit.lua といった vimrc と呼ばれる設定ファイルを置くと、それが起動時に実行されるらしい。世の中的には init.lua で書くのが主流っぽい。

ちなみに NVIM_APPNAME=nvim-test nvim のようにして Neovim を起動すると、~/.config/nvim-test が設定ファイルの置き場所として使われる。デバッグ目的で初期設定の Neovim を起動したい時や、設定を切り替えたい時に便利。

Lua の書き方

init.lua は Lua で書く。以下の公式ドキュメントを読んで書き方を学ぶと良い。

LLM に「<あなたがよく使う言語> を書く人向けに Lua の記法や作法、違いを簡潔に教えてもらえますか?」と聞くのも良い。

実際に実行しながら挙動を確かめたいなら、Neovim のノーマルモードで :lua print("Hello!") などと打ち込むと良い。Lua を実行できる。

Neovim の LSP サポートについて

Neovim は LSP のサポートを組み込んでいる。ただし、デフォルトではどの Language Server も起動されないようになっている。ユーザが Language Server を起動するための設定が必要になる。

まず Language Server をインストールする必要がある。TypeScript の Language Server は typescript-language-servervtsls の二種類がある。今回は typescript-language-server を使ってみる。以下のコマンドでインストールできる。

npm i -g typescript-language-server typescript

そして init.lua に以下のような設定を書く。

-- 補完を有効にするための設定
vim.cmd[[set completeopt+=menuone,noselect,popup]]
vim.api.nvim_create_autocmd('LspAttach', {
  callback = function(ev)
    local client = vim.lsp.get_client_by_id(ev.data.client_id)
    if client and client:supports_method('textDocument/completion') then
      vim.lsp.completion.enable(true, client.id, ev.buf, { autotrigger = true })
    end
  end,
})

-- TypeScript の Language Server の設定
vim.lsp.config('ts_ls', {
  init_options = { hostInfo = 'neovim' },
  cmd = { 'typescript-language-server', '--stdio' },
  filetypes = {
    'javascript',
    'javascriptreact',
    'javascript.jsx',
    'typescript',
    'typescriptreact',
    'typescript.tsx',
  },
  root_markers = { 'tsconfig.json', 'jsconfig.json', 'package.json', '.git' },
})

-- TypeScript の Language Server を有効に
vim.lsp.enable('ts_ls')

何故か補完を有効にするには vim.api.nvim_create_autocmd が必要らしい。デフォルトで補完有効になってて良い気がするけど、なんでそうなってないんでしょうね。

VS Code ユーザなら以下のように CTRL-Space で補完ウインドウが出るようにしておくと便利だと思う。

vim.keymap.set('i', '<c-space>', function()
  vim.lsp.completion.get()
end)

その他 LSP に関する設定は以下を参照。LSP 関連のキーバインドもここに書いてある。

nvim-lspconfig

世の中の人皆同じような設定を書くので、よく使われる Language Server の共通設定がまとめられた nvim-lspconfig Package が存在する。これを使うのがデファクトらしい。

早速 nvim-lspconfig の使い方を紹介したいところだが、その前に「Plugin」と「Package」という用語について説明する。

「Plugin」と「Package」

どちらも Vim に由来する用語である。「Plugin」は Vim スクリプトファイルをまとめたもので、「Package」は Plugin を含むディレクトリを指す。

Package は ~/.config/nvim/pack/ に以下のようなディレクトリ構成で配置される。

~/.config/nvim/pack/
├─ nvim/
│  ├─ start/
│  │  ├─ start-pkg-1/
│  │  │  └─ plugin/
│  │  │     └─ start-pkg-1.lua
│  │  └─ start-pkg-2/
│  │     └─ plugin/
│  │        └─ start-pkg-2.lua
│  └─ opt/
│     ├─ opt-pkg-1/
│     │  └─ plugin/
│     │     └─ opt-pkg-1.lua
│     └─ opt-pkg-2/
│        └─ plugin/
│           └─ opt-pkg-2.lua
└─ multiple-plugins-pkg/
   └─ start/
      ├─ plugin-1/
      │  └─ multiple-plugins-pkg-plugin-1.lua
      └─ plugin-2/
         └─ multiple-plugins-pkg-plugin-2.lua

pack/nvim/start/* にあるのが「"start" package」と呼ばれるもので、Neovim の起動時に自動で実行される *1pack/nvim/opt/* にあるのが「"opt" package」で、:packadd した時に実行される。ちなみに Package は Plugin を複数持つこともできて、その例が pack/multiple-plugins-pkg である *2。なんだかややこしいが、pack/* にあるものも、pack/*/{start,opt}/* にあるものも Package と呼ぶらしい? とにかく Plugin を含むディレクトリなら Package と呼ぶ世界観に見える。id:mizdra の理解が間違っていたら誰かツッコんでほしい!

nvim-lspconfig のインストールとセットアップ

話を戻すと、nvim-lspconfig は Package である。よってこれをインストールする必要がある。nvim-lspconfig の README にインストール方法がいくつか書かれているので、そのどれかの方法でインストールすれば良い。

ミニマムにやりたいなら git clonevim.pack を使う方法が良い。後者は Neovim の組み込みの Plugin Manager *3 なのだが、まだ実験的な機能で stable では使えない。そこで今回は git clone を使ってインストールしてみる。

git clone https://github.com/neovim/nvim-lspconfig ~/.config/nvim/pack/nvim/start/nvim-lspconfig

これで LSP の設定を以下のように記述できる。

-- 補完を有効にするための設定
vim.cmd[[set completeopt+=menuone,noselect,popup]]
vim.api.nvim_create_autocmd('LspAttach', {
  callback = function(ev)
    local client = vim.lsp.get_client_by_id(ev.data.client_id)
    if client and client:supports_method('textDocument/completion') then
      vim.lsp.completion.enable(true, client.id, ev.buf, { autotrigger = true })
    end
  end,
})

-- TypeScript の Language Server を有効に
vim.lsp.enable('ts_ls')

これだけみると、「nvim-lspconfig から提供される Language Server の設定はどこで読み込んでるんだ?」とびっくりするかも (id:mizdra はびっくりした)。実は vim.lsp.enable('ts_ls') を実行した時に読み込まれている。Package 内の lsp/* から Language Server の設定が自動で探索される挙動となってて、結果として nvim-lspconfig/lsp/ts_ls.lua が読み込まれている。

@css-modules-kit/ts-plugin のセットアップ

@css-modules-kit/ts-plugin は TypeScript Language Service Plugin なので、TypeScript の Language Server に読み込ませる、というステップが必要になる。

まずは @css-modules-kit/ts-plugin をインストールしておく。

npm i -g @css-modules-kit/ts-plugin

そして以下のように init.lua を書く。

-- 補完を有効にするための設定
vim.cmd[[set completeopt+=menuone,noselect,popup]]
vim.api.nvim_create_autocmd('LspAttach', {
  callback = function(ev)
    local client = vim.lsp.get_client_by_id(ev.data.client_id)
    if client and client:supports_method('textDocument/completion') then
      vim.lsp.completion.enable(true, client.id, ev.buf, { autotrigger = true })
    end
  end,
})

-- @css-modules-kit/ts-plugin を使うための設定
local npm_root = vim.trim(vim.fn.system('npm root -g'))
local ts_ls_default = vim.lsp.config.ts_ls
vim.lsp.config('ts_ls', {
  filetypes = vim.list_extend(ts_ls_default.filetypes, { 'css' }),
  init_options = {
    plugins = {
      {
        name = '@css-modules-kit/ts-plugin',
        location = npm_root,
        languages = { 'css' },
      },
    },
  },
})

-- TypeScript の Language Server を有効に
vim.lsp.enable('ts_ls')

やっていることとしては、

  • @css-modules-kit/ts-plugin を plugin として読み込むように
    • init_options.plugins の部分
    • これで *.ts,*.tsx 上で CSS Modules 向けの言語機能が使えるようになる
  • CSS ファイル の Language Server として typescript-language-server が使われるように
    • filetypes = ... の部分
    • これで *.css 上で CSS Modules 向けの言語機能が使えるようになる

filetypes = ... の部分がないと *.css に対する LSP の Request (textDocument/definitiontextDocument/rename) が一切 typescript-language-server に届かず、CSS 上で色々な言語機能が機能しなくなってしまう。*.ts, *.css どちらの LSP Request も受け取れるように、このような設定が必要になる。

これで @css-modules-kit/ts-plugin が Neovim でも動くようになった。めでたしめでたし。

youtu.be

おまけ: vim.lsp.config の挙動

vim.lsp.config('ts_ls', {...}) と書くと、Pacakge 経由で読み込まれた config などといい感じにマージされる仕様となってる。

マージは vim.tbl_deep_extend() で行われる。テーブル (JavaScript のオブジェクトに相当) 同士をディープマージしてくれる。テーブルは再起的に辿ってマージしてくれるけど、配列はたどってくれないので、注意が必要 *4

今回書いた設定で filetypes: vim.list_extend(ts_ls_default.filetypes, { 'css' }) と書いたのはそのため。filetypes: { 'css' } だと typescript-language-server が *.ts の Language Server として使われなくなってしまう。

おまけ: Language Server のデバッグテクニック

Neovim で :checkhealth と打つと実際に読み込まれた Language Server の起動オプションを表示できる。うまく設定できてるのか確かめる時に便利。

:checkhealth を実行した様子

TypeScript Language Service Plugin や typescript-language-server が出力するログを見るには、tsserver.log を出力すると良い。TSS_LOG 環境変数を設定して Neovim を起動すれば良い。

TSS_LOG="-level verbose -file /tmp/tsserver.log" nvim .

*1:https://neovim.io/doc/user/pack.html#_creating-vim-packages でそう呼ばれてる

*2:公式ドキュメントでも https://neovim.io/doc/user/pack.html#_creating-vim-packages でこうした Package を紹介してる。

*3:ところでなんで「Package Manager」と呼ばないのでしょうね?

*4:ちなみに Lua において配列はテーブルの特殊系 (数値をインデックスに持つテーブル) で、厳密には配列もテーブルである。vim.tbl_deep_extend ではテーブルのインデックスを見て、配列っぽいテーブルなら再起的に辿らず、それ以外のテーブルなら辿る挙動になってる。ref: https://github.com/neovim/neovim/blob/6b4ec2264e1d8ba027b85f3883d532c5068be92a/runtime/lua/vim/_core/shared.lua#L552

ポケットモンスター・ポケモン・Pokémon・は任天堂・クリーチャーズ・ゲームフリークの登録商標です.

当ブログは @mizdra 個人により運営されており, 株式会社ポケモン及びその関連会社とは一切関係ありません.