## 経緯 [[IDE]]としての機能には[[coc.nvim]]を使っていた。正直、実用面では不自由なくとても便利だと思う... が、以下の理由から[[LSP]]にチャレンジしてみたくなった。 - [[Neovim]]の標準搭載機能であり、[[Neovim]]使いではそちらの方が推奨されている雰囲気 - [[Lspsaga]]が格好良くて便利そう - リアルの[[Vimmer]]を見てても、[[coc.nvim]] -> [[LSP]]は誰もが通っている雰囲気 ## 前提 - [[Ubuntu 22.04 LTS]] - [[Windows 11]]の[[WSL]]内で利用 - [[Neovim]]のバージョンは0.9.5 ## 方針と要件 すべて移行すると大変そうなので、まずは[[📒Obsidianプラグイン開発]]でも利用する[[TypeScript]] ([[Node.js]], [[Bun]]) あたりから攻める。 - [*] ベースの調査と作成 - [x] [[Neovim起動時に設定で定義された対象がインストールされていなければmason.nvimでインストール]] - [x] lspconfigの名前に関する問題について - [x] オートコンプリートできるようにする - [x] [[nvim-cmp]] - [x] 保存したら自動フォーマット - [x] Code actionの選択がめんどい.. Enterでほいっとしたい - [x] [[Lspsaga]]を入れる - [x] [[Lspsaga]] - [x] `ESC`でcode_actionのフローティングは消す - [*] https://zenn.dev/botamotch/articles/21073d78bc68bf - [x] `reference highlight` - [x] [[illuminate.vim]]で対応 - [ ] `IlluminatedWordWrite` ができない - [x] snippets展開とplaceholder移動 ([[LuaSnip]]) - [x] サジェストに枠線を出したい - [ ] 補完アイコンを表示する ## ベースの作成 まずはベース部分の知識習得と作成が必要。とりあえずいじってみてから理解するようにしてみる。 `init.lua` ```lua { "neovim/nvim-lspconfig", "williamboman/mason.nvim", require("plugins.mason-lspconfig"), } ``` `plugins/mason-lspconfig` ```lua return { "williamboman/mason-lspconfig.nvim", dependencies = { { "williamboman/mason.nvim" }, { "neovim/nvim-lspconfig" }, }, config = function() require("mason").setup() require("mason-lspconfig").setup() end } ``` ### TypeScript Language Serverをインストール まずは[[mason.nvim]]で[[LSP]]のインストールが必要なはず。[[TypeScript Language Server]]でよさそう。 ![[Pasted image 20240128115423.png]] ### 起動時にインストールできるようにする [[Neovim起動時に設定で定義された対象がインストールされていなければmason.nvimでインストール]]できるような対応を入れて、[[TypeScript Language Server]]のみをインストールしてみた。 ```lua { "williamboman/mason-lspconfig.nvim", dependencies = { { "williamboman/mason.nvim" }, { "neovim/nvim-lspconfig" }, }, config = function() require("mason").setup() -- https://github.com/williamboman/mason.nvim/issues/1309#issuecomment-1555018732 local registry = require "mason-registry" local packages = { "typescript-language-server", } registry.refresh(function () for _, pkg_name in ipairs(packages) do local pkg = registry.get_package(pkg_name) if not pkg:is_installed() then pkg:install() end end end) require("mason-lspconfig").setup() require("lspconfig").tsserver.setup {} end } ``` とりあえず何か出るようにはなった。 ![[Pasted image 20240128122912.png]] ## キーバインド設定 [[nvim-lspconfig]]のREADMEに親切な案内があったので参考にする。[[nvim-lspconfig]]自体はキーマップに関する機能を提供していないので、参考程度に...とのこと。 <div class="link-card-v2"> <div class="link-card-v2-site"> <img class="link-card-v2-site-icon" src="https://github.githubassets.com/favicons/favicon.svg" /> <span class="link-card-v2-site-name">GitHub</span> </div> <div class="link-card-v2-title"> GitHub - neovim/nvim-lspconfig: Quickstart configs for Nvim LSP </div> <div class="link-card-v2-content"> Quickstart configs for Nvim LSP. Contribute to neovim/nvim-lspconfig development by creating an account on GitHu ... </div> <img class="link-card-v2-image" src="https://opengraph.githubassets.com/57abc3a3d0e19ad9034809eaff0215dc656d777e14802af775391f95696e2b43/neovim/nvim-lspconfig" /> <a href="https://github.com/neovim/nvim-lspconfig?tab=readme-ov-file#suggested-configuration"></a> </div> とりあえずこんな感じ。 ```lua vim.api.nvim_create_autocmd('LspAttach', { group = vim.api.nvim_create_augroup('UserLspConfig', {}), callback = function(ev) -- Enable completion triggered by <c-x><c-o> vim.bo[ev.buf].omnifunc = 'v:lua.vim.lsp.omnifunc' local opts = { buffer = ev.buf } -- 定義に移動 vim.keymap.set('n', '<C-]>', vim.lsp.buf.definition, opts) -- 定義をホバー vim.keymap.set('n', '<M-s>', vim.lsp.buf.hover, opts) -- 実装へ移動 vim.keymap.set('n', '<C-j>i', vim.lsp.buf.implementation, opts) -- 関数の引数表示 vim.keymap.set({'n', 'i'}, '<C-p>', vim.lsp.buf.signature_help, opts) -- リネーム vim.keymap.set({'n', 'i'}, '<S-M-r>', vim.lsp.buf.rename, opts) -- Code action (TODO: 楽にしたい) vim.keymap.set({'n', 'i'}, '<M-CR>', vim.lsp.buf.code_action, opts) -- 呼び出し元一覧 (qflist) vim.keymap.set('n', '<C-j>h', vim.lsp.buf.references, opts) -- 次の診断へ移動 vim.keymap.set('n', '<M-j>', vim.diagnostic.goto_next, opts) -- 前の診断へ移動 vim.keymap.set('n', '<M-k>', vim.diagnostic.goto_prev, opts) -- TODO: 一旦このまま vim.keymap.set('n', '<space>f', function() vim.lsp.buf.format { async = true } end, opts) end, }) ``` ## オートコンプリートの設定 [[Neovimは標準機能でオートコンプリートを提供していない]]ため、オートコンプリートを利用する場合は別のプラグインが必要になる。 <div class="link-card-v2"> <div class="link-card-v2-site"> <img class="link-card-v2-site-icon" src="https://github.githubassets.com/favicons/favicon.svg" /> <span class="link-card-v2-site-name">GitHub</span> </div> <div class="link-card-v2-title"> Autocompletion </div> <div class="link-card-v2-content"> Quickstart configs for Nvim LSP. Contribute to neovim/nvim-lspconfig development by creating an account on GitHu ... </div> <img class="link-card-v2-image" src="https://opengraph.githubassets.com/57abc3a3d0e19ad9034809eaff0215dc656d777e14802af775391f95696e2b43/neovim/nvim-lspconfig" /> <a href="https://github.com/neovim/nvim-lspconfig/wiki/Autocompletion"></a> </div> 推奨が[[nvim-cmp]]なのでインストールして設定する。[[cmp-nvim-lsp]]も。 mason系の設定が終わった後に[[cmp-nvim-lsp]]から設定する。 ```lua config = function() -- mason.nvim setup require("mason").setup() local packages = { "typescript-language-server", } -- https://github.com/williamboman/mason.nvim/issues/1309#issuecomment-1555018732 local registry = require "mason-registry" registry.refresh(function() for _, pkg_name in ipairs(packages) do local pkg = registry.get_package(pkg_name) if not pkg:is_installed() then pkg:install() end end end) require("mason-lspconfig").setup() -- Loading nvim-cmp local capabilities = require("cmp_nvim_lsp").default_capabilities() local lspconfig = require('lspconfig') local servers = { "tsserver", } for _, lsp in ipairs(servers) do lspconfig[lsp].setup { -- on_attach = my_custom_on_attach, capabilities = capabilities, } end -- nvim-cmp key bindings local cmp = require("cmp") cmp.setup { mapping = cmp.mapping.preset.insert({ ['<F5>'] = cmp.mapping.complete(), ['<CR>'] = cmp.mapping.confirm { behavior = cmp.ConfirmBehavior.Replace, select = true, }, ['<C-p>'] = cmp.mapping.abort(), }), sources = { { name = 'nvim_lsp' }, }, } -- LSP key bindings vim.api.nvim_create_autocmd('LspAttach', { 中略 ``` `<C-p>`は`vim.lsp.buf.signature_help`と被るので無効化した。 > [[特定のキーマップを無効 (nvim-cmp)|特定のキーマップを無効]] ## 保存したら自動でフォーマットする ### 手動フォーマット まずは手動フォーマットできるようにする。 [[none-ls.nvim]]を使う。 ```lua { "nvimtools/none-ls.nvim", config = function() local null_ls = require("null-ls") null_ls.setup({ sources = { null_ls.builtins.formatting.prettier, }, }) end } ``` `:lua vim.lsp.buf.format()`でフォーマットがかかる。 基本的にLocalにインストールされた[[Prettier]]なり[[Biome]]なりを使うので、[[mason.nvim]]ではインストールしない。 ### 自動フォーマット [[none-ls.nvim]]のsetup内で`on_attach`に定義する。使用している[[Neovim]]バージョンは0.9なので、`vim.lsp.buf.format({ async = false })`を使う。 ```lua { "nvimtools/none-ls.nvim", config = function() local null_ls = require("null-ls") local augroup = vim.api.nvim_create_augroup("LspFormatting", {}) null_ls.setup({ sources = { null_ls.builtins.formatting.prettier, }, on_attach = function(client, bufnr) if client.supports_method("textDocument/formatting") then vim.api.nvim_clear_autocmds({ group = augroup, buffer = bufnr }) vim.api.nvim_create_autocmd("BufWritePre", { group = augroup, buffer = bufnr, callback = function() vim.lsp.buf.format({ async = false }) end, }) end end, }) end } ``` ## カーソル配下のシンボルをハイライトする [[illuminate.vim]]を使う。 `appearance.nvim` ```lua -- For illuminate.nvim vim.api.nvim_set_hl(0, "IlluminatedWordText", { fg = 'lightgray', bg = 'darkcyan' }) vim.api.nvim_set_hl(0, "IlluminatedWordRead", { fg = 'lightgray', bg = 'darkgreen' }) vim.api.nvim_set_hl(0, "IlluminatedWordWrite", { fg = 'lightgray', bg = 'darkred' }) ``` `IlluminatedWordWrite` であるべきところも `IlluminatedWordRead` になるという点は気になるが、[[illuminate.vim]]特有の問題ではなさそうだし、[[Go]] ([[gopls]]) でも再現するので一旦保留する。 > [[📕Neovimでカーソルのワード(シンボル)をハイライト]] ## [[Lspsaga]] 本当にやりたかったのはここから。遂にきた! ### インストール ```lua { 'nvimdev/lspsaga.nvim', config = function() require('lspsaga').setup({}) end, dependencies = { 'nvim-treesitter/nvim-treesitter', 'nvim-tree/nvim-web-devicons', } } ``` 1つずつ設定を吟味していく。 ### Breadcrumbs https://nvimdev.github.io/lspsaga/breadcrumbs/ あまり役に立ったことはないので削除する。 ### Callhierarchy https://nvimdev.github.io/lspsaga/callhierarchy/ `vim.lsp.buf.references`とは結果の差分がある。 - `incoming_calls`は呼び出し元 **関数** をリストアップ - `vim.lsp.buf.references` は呼び出し元 **行** をリストアップ この違いがあるなら、この2つは用途が異なるかも。UIは`incoming_calls`の方が見やすいが、[[クイックフィックス (Vim)|クイックフィックス]]を使っている`vim.lsp.buf.references`の方が利便性は上かも...。 ### Code Action https://nvimdev.github.io/lspsaga/codeaction/ `vim.lsp.buf.code_action`よりは明らかに便利なので採用する。以下のキーバインドを変更。 ```diff - vim.keymap.set({ 'n', 'i' }, '<M-CR>', vim.lsp.buf.code_action, opts) + vim.keymap.set({ 'n', 'i' }, '<M-CR>', "<cmd>Lspsaga code_action<CR>", opts) ``` 変更点のプレビューが出せる場合は見たいので`extend_gitsigns`も`true`に。 ```lua require('lspsaga').setup({ code_action = { extend_gitsigns = true, }, ``` ### Definition https://nvimdev.github.io/lspsaga/definition/ `vim.lsp.buf.implementation`とは挙動が異なり、[[フローティングウィンドウ (Neovim)|フローティングウィンドウ]]に定義を表示する。軽く実装を確認したいときは便利なので、別のキーバインドで割り当てする。 ```lua -- 実装をホバー vim.keymap.set('n', '<M-d>', "<cmd>Lspsaga peek_definition<CR>", opts) ``` ### Diagnostic https://nvimdev.github.io/lspsaga/diagnostic/ 一番やりたかったのはこれ。 ```diff -- 次の診断へ移動 - vim.keymap.set('n', '<M-j>', vim.diagnostic.goto_next, opts) + vim.keymap.set('n', '<M-j>', "<cmd>Lspsaga diagnostic_jump_next<CR>", opts) -- 前の診断へ移動 - vim.keymap.set('n', '<M-k>', vim.diagnostic.goto_prev, opts) + vim.keymap.set('n', '<M-k>', "<cmd>Lspsaga diagnostic_jump_prev<CR>", opts) ``` なんかUIに下線が引かれまくっている。あと ![[Pasted image 20240128183931.png]] せっかくなのでこの機会にテーマを変えてみる。最近流行っている[[Tokyo Night (Neovim)|Tokyo Night]]にしてみた。すると... ![[Pasted image 20240128194402.png]] いい感じ。[[virtual text (Neovim)|virtual text]]にも色がつくのか。 ### Finder https://nvimdev.github.io/lspsaga/finder/ 対象の参照と実装を同時に確認する機能。実装はともかく参照が見やすいのは助かる。 ![[Pasted image 20240128194603.png]] `<C-j>u`あたりに割り当てておく。Usage。 表示領域を大きくしつつ、[[フローティングウィンドウ (Neovim)|フローティングウィンドウ]]内での移動や決定を他の操作にできるだけあわせて直感的に複数個所を修正できるようにする。 ```lua require('lspsaga').setup({ finder = { max_height = 0.7, left_width = 0.3, right_width = 0.6, keys = { shuttle = "<Space>w", toggle_or_open = "<CR>" } } ``` ### Float Terminal ターミナルは間に合っている気がしなくもないけど、広い領域で[[ノーマルモード]]を使いたい場合に需要がありそうな気がしている... のでお試し期間ということで使ってみる。 ```lua vim.keymap.set('n', '<Space>i', "<cmd>Lspsaga term_toggle<CR>", opts) ``` Float Terminal内でのkeymapはうまくできなかったので設定していないが、`:q`で普通に閉じられる(再開も可能)なので問題ない。 ### Hover `vim.lsp.buf.hover`の上位互換だと思うので置き換えで問題ないはず。 ```diff -- 定義をホバー - vim.keymap.set('n', '<M-s>', vim.lsp.buf.hover, opts) + vim.keymap.set('n', '<M-s>', vim.lsp.buf.hover, opts) ``` ### LightBlub ほとんどのケースで表示されて邪魔なのでオフにする。 ```lua { "nvimdev/lspsaga.nvim", config = function() require("lspsaga").setup({ lightbulb = { enable = false, }, ``` ### Outline [[aerial.nvim]]の方が実用的な気がするので様子見。 ### Rename `vim.lsp.buf.rename`よりも、変更対象の傍に[[フローティングウィンドウ (Neovim)|フローティングウィンドウ]]が表示されるという点で優れている。ので移行。 ```diff -- リネーム - vim.keymap.set({ 'n', 'i' }, '<S-M-r>', vim.lsp.buf.rename, opts) + vim.keymap.set({ 'n', 'i' }, '<S-M-r>', "<cmd>Lspsaga rename<CR>", opts) ``` ## スニペット [[Ultisnips]]はエラーが大量に出たので、[[LuaSnip]]を入れた。 ## 続き > [[📜2024-01-30 mason.nvimを利用せずnvim-lspconfigだけでNeovimのLSPを管理する]] ## 参考 - https://nvimdev.github.io/lspsaga/ - https://scrapbox.io/yyano/coc.nvim_to_nvim-lsp - https://zenn.dev/futsuuu/articles/3b74a8acec166e