Mac と Ubuntu で同じ開発環境を作る dotfiles 設計メモ 第3回:Neovim・VS Code Neovim・IME を共通化する
はじめに
前回は、Ghostty / zsh / tmux の設定を例に、Mac と Ubuntu の OS 差分をどう吸収しているかを書きました。
今回は、Neovim の設定を扱います。
自分の dotfiles では、Neovim を次の2つの環境で使っています。
- ターミナル上の Neovim
- VS Code Neovim
さらに、日本語入力 IME が Esc 後も残る問題にも対応しています。
この記事では、次の内容を書きます。
- Neovim 設定のモジュール化
- VS Code Neovim との両対応
- lazy.nvim と
lazy-lock.jsonによる再現性 - InsertLeave を使った IME 自動オフ
検証環境
この記事の内容は、以下のような環境を前提にしています。
| 項目 | 環境 |
|---|---|
| macOS | TODO: 例 macOS 15.x
|
| Ubuntu | TODO: 例 Ubuntu 24.04 LTS
|
| Ubuntu のセッション | X11 |
| エディタ | Neovim |
| VS Code 拡張 | VS Code Neovim |
| プラグイン管理 | lazy.nvim |
| Linux 側の IME | TODO: 例 fcitx5
|
| macOS 側の IME 切り替え | TODO: 例 macism
|
Ubuntu 側は X11 前提です。
Neovim 設定の構成
Neovim の設定は Lua で書き、役割ごとにファイルを分割しています。
init.lua は読み込みの司令塔に徹し、実体は core/ と plugins/ に分けています。
nvim/
└── .config/
└── nvim/
├── init.lua
├── lua/
│ ├── core/
│ │ ├── options.lua
│ │ ├── keymaps.lua
│ │ ├── autocmds.lua
│ │ └── lazy.lua
│ ├── plugins/
│ │ ├── editor.lua
│ │ ├── lsp.lua
│ │ └── ui.lua
│ └── vsc/
│ └── keymaps.lua
└── lazy-lock.json
init.lua は次のようにしています。
require("core.options")
require("core.keymaps")
require("core.autocmds")
require("core.lazy")
if vim.g.vscode then
require("vsc.keymaps")
end
init.lua にすべてを書くのではなく、役割ごとに分けています。
自分の場合は、だいたい次のように分けています。
core/options.lua 基本オプション
core/keymaps.lua 共通キーマップ
core/autocmds.lua 自動コマンド
core/lazy.lua lazy.nvim の初期化
plugins/*.lua プラグイン定義
vsc/keymaps.lua VS Code Neovim 用キーマップ
こうしておくと、あとから設定を見返したときに、どこを触ればよいか分かりやすくなります。
VS Code Neovim との両対応
Neovim はターミナルでも使いますが、VS Code Neovim でも使っています。
VS Code Neovim で起動した場合は vim.g.vscode が立つので、それを見て VS Code 用の設定を読み込んでいます。
if vim.g.vscode then
require("vsc.keymaps")
end
これにより、ターミナル Neovim では通常の設定を使いつつ、VS Code Neovim では VS Code に合わせたキーマップを追加できます。
プラグインを環境ごとに読み分ける
プラグインも、ターミナル Neovim と VS Code Neovim で読み分けています。
例えば、ターミナル Neovim では EasyMotion を使い、VS Code Neovim では別のプラグインを使う、というような分け方です。
{
"easymotion/vim-easymotion",
cond = function()
return vim.g.vscode == nil
end,
},
{
"smoka7/hop.nvim",
cond = function()
return vim.g.vscode ~= nil
end,
},
ポイントは、VS Code Neovim 上では不要なプラグインまで読み込まないことです。
同じ init.lua を使いながら、実行環境によって必要な設定だけを読み込むようにしています。
lazy-lock.json で再現性を持たせる
プラグイン管理には lazy.nvim を使っています。
dotfiles を複数マシンで使う場合、「設定ファイルが同じ」だけでは不十分です。
プラグインのバージョンが違うと、同じ設定でも挙動が変わることがあります。
そこで、lazy-lock.json をリポジトリに含めています。
lazy-lock.json によって、各プラグインのコミットハッシュを固定できます。
これは package-lock.json と同じような考え方で、別マシンでも同じバージョンの環境を再現しやすくするためです。
新しい環境では、プラグインを同期したあとに lockfile の状態へ戻すことで、できるだけ同じ状態に揃えられます。
:Lazy sync
:Lazy restore
dotfiles を育てていくうえで、再現性はかなり大事だと感じています。
ハマった課題:日本語入力 IME が Esc 後も残る
一番「自分ごと」として取り組んだのが、Neovim での日本語入力問題です。
インサートモードで日本語を打ち、Esc でノーマルモードに戻っても IME が ON のままだと、続くキー操作が文字入力として扱われてしまいます。
例えば、ノーマルモードで j や k を押したいのに、日本語入力中の文字として食われることがあります。
これは、Vim のモードと IME の状態が独立しているために起きる問題です。
Vim のモード:
Insert mode / Normal mode
OS の IME:
ON / OFF
Vim 側が Normal mode に戻っても、OS 側の IME が自動で OFF になるわけではありません。
そのため、Vim のモード遷移に合わせて IME を明示的に OFF にする必要があります。
InsertLeave で IME をオフにする
解決策は、インサートモードを抜けるタイミングで IME を強制的にオフにすることでした。
Neovim では InsertLeave の autocmd を使えます。
local ime_group = vim.api.nvim_create_augroup("IMEAutoToggle", { clear = true })
vim.api.nvim_create_autocmd("InsertLeave", {
group = ime_group,
callback = function()
if vim.fn.executable("fcitx5-remote") == 1 then
-- Linux / fcitx5
vim.fn.system({ "fcitx5-remote", "-c" })
elseif vim.fn.has("macunix") == 1 and vim.fn.executable("macism") == 1 then
-- macOS / macism
vim.fn.system({ "macism", "com.apple.keylayout.ABC" })
end
end,
})
最初は Linux 用に fcitx5-remote -c をそのまま呼んでいました。
vim.fn.system("fcitx5-remote -c")
しかし、この書き方だと macOS や fcitx5-remote が入っていない環境で失敗します。
そこで、vim.fn.executable() や vim.fn.has("macunix") を使って、使えるコマンドがある場合だけ実行するようにしました。
Ctrl-C で抜ける場合の注意
この設定は、Esc でインサートモードを抜ける運用を前提にしています。
Ctrl-C でインサートモードを抜ける場合は、InsertLeave が発火しないため、挙動が変わります。
普段から Ctrl-C で抜ける人は、別のイベントやキーマップで対応する必要があります。
自分は Esc で抜ける運用なので、InsertLeave で十分でした。
IME 対応で理解したこと
この問題に取り組むまで、IME はエディタ側で何となく制御されているものだと思っていました。
しかし実際には、Vim のモードと OS の IME は独立しています。
そのため、両者の状態を揃えるには、どこかで明示的に橋渡しする必要があります。
Insert mode を抜ける
-> InsertLeave が発火する
-> fcitx5-remote / macism を呼ぶ
-> OS の IME を OFF にする
この構造を理解したことで、「なぜ Esc 後に日本語入力が残るのか」が腑に落ちました。
症状だけを消すのではなく、仕組みを理解してから直せたのが大きかったです。
今回のまとめ
今回は、Neovim / VS Code Neovim / IME の設定について書きました。
やったことをまとめると、次の通りです。
- Neovim 設定を
core/、plugins/、vsc/に分割した -
init.luaは読み込みの司令塔にした -
vim.g.vscodeで VS Code Neovim かどうかを判定した - プラグインを
condで環境ごとに読み分けた -
lazy-lock.jsonを管理してプラグインバージョンを固定した -
InsertLeaveで IME を自動的に OFF にした -
executable()や OS 判定で、存在しないコマンドを呼ばないようにした
Neovim の設定は、放っておくとすぐに巨大な init.lua になりがちです。
役割ごとに分け、実行環境ごとの違いを小さな条件分岐に閉じ込めることで、Mac と Ubuntu、さらに VS Code Neovim でも扱いやすくなりました。
シリーズ全体のまとめ
3回に分けて、Mac と Ubuntu で同じ開発環境を作るための dotfiles 設計について書きました。
振り返ると、設定ファイルを両対応にしようとするだけで、次のような点を調べることになりました。
- ディストリビューションやインストール方法でファイルの置き場所が変わること
- Homebrew の prefix が環境によって異なること
-
/usr/shareと/usr/local/shareの使い分け - 設定を
~/.config配下にまとめる XDG Base Directory の考え方 -
unameや環境変数で実行環境を判定する方法 - X11 / Wayland でクリップボード連携の方法が変わること
- tmux の copy-mode と OS クリップボードの関係
- Vim のモードと IME の状態が独立していること
- lockfile でプラグインのバージョンを固定し、環境を再現可能にすること
どれも単体では小さな知識ですが、「同じ操作感をどの環境でも使いたい」という目標を追ううちに、自然と必要になったものです。
dotfiles は一度作って終わりではなく、新しいツールを試したり、別の OS に移ったりするたびに少しずつ手を入れていくものだと感じています。
設定ファイルは地味ですが、自分の道具をどう理解し、どう手入れしているかがそのまま表れる場所だと思っています。
同じように環境を育てている方の参考になれば嬉しいです。
今後やりたいこと
今後は、次のあたりを整備したいと考えています。
- dotfiles のインストールスクリプト化
- OS ごとの symlink 展開を自動化する
- macOS / Ubuntu で必要なパッケージ一覧を管理する
- Neovim のプラグイン更新手順を整理する
- 設定ファイルのテストや lint を導入する
- 公開用 dotfiles と非公開設定の境界をより明確にする