Solist Work Blog

Software engineer note

EglotでLanguage Server Protocolを使う

EmacsのLanguage Server Protocolクライアントであるeglotを使って半年くらいが経過し、 Language Server Protocolに移行したほうがよいと思うにいたったのでそろそろBlogに書いていくことにします。

Language Server Protocolのよいところ

eglotで補完している画像

まず、Language Server Protocolに移行してよかったことをあげると、Visual Studio CodeやIDEと同じ補完や定義ジャンプを使えるようになることです。 Emacsのpythonの開発環境のelpyやruby開発環境のrobeはとてもよいのですが、少し動作が重いのも事実です。 eglotpython-language-serverを使うとelpyより軽快に動くようになり快適になりました。 robeよりsolargraphが軽くて快適に使えます。 Language Server Protocolは共通のプロトコルを作って開発者のリソースを節約する目的で作られているものですから将来性に心配はありませんし、おそらく銀の弾丸1になることは間違いないでしょう。 gocodeは使えなくなったので移行せぜるを得ませんし、もはやLanguage Server Protocolを使わない理由などないと思います。

smart-jumpでeglotと共存する

Language Server Protocolの欠点はソースコードが巨大すぎると重いということでしょうか。ほとんどの場合Language Server Protocolで問題ないと思いますが、ソースコードが巨大なプロジェクトではLanguage Server Protocolを使わないでTag系のツールを使えばよいでしょう。今でもchromiumのような巨大なソースコードではIDEは使い物になりませんが、Emacsではこの場合でもなんとかすることができるのです。Language Server ProtocolとTag系のツールをEmacsで共存させると巨大なソースコードでも対処できるようになります。 その目的のためにsmart-jumpを使います。 Emacs25以降では関数やクラスや変数の上でM-.を押すとその定義へジャンプすることができます。 M-.を押した時、smart-jumpはまずLanguage Server Protocolで定義ジャンプするように試みてくれます。Language Server Protocolが失敗すると次にデフォルトで指定されたものかユーザーが指定したTag系のツールなどで定義ジャンプするように試みます。それも失敗すると最後にdumb-jumpで定義ジャンプするようになっています。 dumb-jumpThe Silver Searcherripgrep、grepで定義ジャンプするのでフェイルオーバーの最終ランナーに相応しいといえるでしょう。 話を巨大なソースコードに戻すとeglotをオフにしておく2smart-jumpの機能でM-.を押した時はTag系のツールで定義ジャンプすることができます。rtagsを定義ジャンプに使えるし、ggtagsの関数を利用してGNU GLOBALなどで定義ジャンプすることもできます。すべての場合においてM-. M-, M-?3で対処できるようになるのです。これは使わない手はないでしょう。

eglotを使えるようにする

Language Server Protocolを使うためにはcompany-modeがほぼ標準です。auto-completeをつかっている場合は乗り換えましょう。

M-x package-install company
M-x package-install company-quickhelp
M-x package-install bind-key

company-modeの設定例

(require 'company)
(require 'bind-key)
(setq company-minimum-prefix-length 2)
(setq company-selection-wrap-around t)
(bind-key "C-M-i" 'company-complete)
(bind-key "C-h" nil company-active-map)
(bind-key "C-n" 'company-select-next company-active-map)
(bind-key "C-p" 'company-select-previous company-active-map)
(bind-key "C-n" 'company-select-next company-search-map)
(bind-key "C-p" 'company-select-previous company-search-map)
(bind-key "<tab>" 'company-complete-common-or-cycle company-active-map)
(bind-key "<backtab>" 'company-select-previous company-active-map)
(bind-key "C-i" 'company-complete-selection company-active-map)
(bind-key "M-d" 'company-show-doc-buffer company-active-map)
(add-hook 'after-init-hook 'global-company-mode)
(setq company-tooltip-maximum-width 50)

;; company-quickhelp
(setq company-quickhelp-color-foreground "white")
(setq company-quickhelp-color-background "dark slate gray")
(setq company-quickhelp-max-lines 5)
(company-quickhelp-mode)

smart-jumpをインストールするとdumb-jumpも依存でインストールされます。

M-x package-install eglot
M-x package-install smart-jump
M-x package-install exec-path-from-shell

exec-path-from-shellの設定

(when (memq window-system '(mac ns x))
  (exec-path-from-shell-initialize))
(setq exec-path-from-shell-check-startup-files nil)

The Silver Searcherripgrepとgrepをインストールします。例はArchLinuxです。

sudo pacman -S the_silver_searcher ripgrep grep

dumb-jumpsmart-jumpの設定例
この例ではivyを使うのでivyのインストールも必要です

;; dumb-jump
(dumb-jump-mode)
(setq dumb-jump-selector 'ivy)

;; smart-jump
(smart-jump-setup-default-registers)

次に使いたいプログラミング言語のlanguage-serverをインストールします。

# python
pip install --user python-language-server
pip install --user rope
pip install --user pyflakes
pip install --user yapf
pip install --user autopep8

# go
go get -u -v golang.org/x/tools/cmd/gopls
go get -u -v golang.org/x/tools/cmd/goimports

# typescript javascript
yarn global add javascript-typescript-langserver

# ruby
gem install solargraph

# dart
sudo pacman -S dart
pub global activate dart_language_server

# rust
sudo pacman -S rustup
rustup default stable
rustup component add rls rust-analysis rust-src

.bashrc or .zshrcにPATHの設定をする

# go
export GOPATH=$HOME
export PATH="$PATH:$GOPATH/bin"
# ruby
PATH="$(ruby -e 'print Gem.user_dir')/bin:$PATH"
# typescript javascript
PATH="$HOME/.node_modules/bin:$PATH"
export npm_config_prefix=~/.node_modules
# python
PATH="$HOME/.local/bin:$PATH"
# dart
export PATH="$PATH:$HOME/src/github.com/flutter/flutter/bin"

pythonの例

M-x package-install python-mode
(add-hook 'python-mode-hook 'eglot-ensure)

(add-to-list 'eglot-server-programs
	     `(python-mode . ("pyls" "-v" "--tcp" "--host"
			      "localhost" "--port" :autoport)))

goの例

M-x package-install go-mode
(add-hook 'go-mode-hook 'eglot-ensure)
(setq gofmt-command "goimports")
(add-hook 'before-save-hook #'gofmt-before-save)
(with-eval-after-load 'eglot
  (add-to-list 'eglot-server-programs '(go-mode . ("gopls"))))

javascript typescriptの例

M-x package-install js2-mode
M-x package-install typescript-mode
(autoload 'js2-mode "js2-mode" nil t)
(add-to-list 'auto-mode-alist '("\.js$" . js2-mode))

(add-hook 'js2-mode-hook 'eglot-ensure)
(add-hook 'typescript-mode-hook 'eglot-ensure)

rubyの例


(add-hook 'ruby-mode-hook 'eglot-ensure)

;; projectile rails
(projectile-rails-global-mode)

dartの例

M-x package-install dart-mode

Flutterをインストールしておく。

mkdir -p ~/src/github.com/flutter
cd ~/src/github.com/flutter
wget https://storage.googleapis.com/flutter_infra/releases/stable/linux/flutter_linux_v1.9.1+hotfix.6-stable.tar.xz
tar xf flutter_linux_v1.9.1+hotfix.6-stable.tar.xz

(add-hook 'dart-mode-hook 'eglot-ensure)

(setq flutter-reload-flg nil)

(defun flutter-reload()
  "Send a signal to daemon."
  (start-process "flutter-reloader" nil "pkill" "-SIGUSR1" "-f" "flutter_tool"))

(defun flutter-reload-mode()
  "Flutter reload mode."
  (interactive)
  (if flutter-reload-flg
      (progn
	(remove-hook 'after-save-hook 'flutter-reload t)
	(setq flutter-reload-flg nil)
	(message "Flutter-reload-mode disabled"))
    (add-hook 'after-save-hook 'flutter-reload t t)
    (setq flutter-reload-flg t)
    (message "Flutter-reload-mode enabled")))

(add-hook 'dart-mode-hook 'flutter-reload-mode)

rustの例

M-x package-install rust-mode

(add-hook 'rust-mode-hook 'eglot-ensure)

elixirの例。まずelixir-lsをインストールする。elixirのバージョンが変わると再コンパイルが必要です。

mkdir -p ${HOME}/src/github.com/JakeBecker
cd ${HOME}/src/github.com/JakeBecker
git clone git@github.com:JakeBecker/elixir-ls.git
cd elixir-ls && mkdir rel
mix deps.get && mix compile
mix elixir_ls.release -o rel
M-x package-install elixir-mode

.bashrc or .zshrcでelixir-lsにPATHを通す

export PATH="$PATH:$HOME/src/github.com/JakeBecker/elixir-ls/rel/"
(add-hook 'elixir-mode-hook 'eglot-ensure)

基本的には各プログラミング言語のメジャーモードが拡張子で有効になるので、それをフックにしてeglotを自動で起動します。eglotが起動すると各プログラミング言語のlanguage-serverはeglotがeglot-server-programsを見て起動してくれるのでEmacsで対象言語のファイルを開くだけで使えるようになります。 smart-jumpのデフォルトの定義が気にいらない場合はsmart-jumpの設定をREADMEを参考にして追加してください。

ivy-xrefを使っているところ

xrefの候補が複数ある場合ivy-xrefがお勧めです。 ivy-xrefで画像のようになります。

M-x package-install ivy-xref

ivy-xrefの設定例

(setq xref-show-xrefs-function #'ivy-xref-show-xrefs)

flymakeの設定

flymakeでlint系のチェックをする

以下のような設定をするとこの画像のようにミニバッファから少し上にflymakeのエラー表示の場所を変更できるのでeldocなどと競合しないようにできます。 flycheck-posframeを使うとflycheckの結果も同時に表示できますが、 Language Server Protocolを利用するflymakeのほうが結果がよかったので、両方利用するのは無駄であると判断しflycheckは消すことにしました。

M-x package-install flymake-diagnostic-at-point
M-x package-install posframe
;; flymake
(require 'flymake-diagnostic-at-point)
(with-eval-after-load 'flymake
  (add-hook 'flymake-mode-hook #'flymake-diagnostic-at-point-mode)
  (add-hook 'emacs-lisp-mode-hook #'package-lint-setup-flymake)
  (set-face-attribute 'popup-tip-face nil
		      :background "dark slate gray" :foreground "white" :underline nil))
(remove-hook 'flymake-diagnostic-functions 'flymake-proc-legacy-flymake)

;; flymake-posframe
(defvar flymake-posframe-hide-posframe-hooks
  '(pre-command-hook post-command-hook focus-out-hook)
  "The hooks which should trigger automatic removal of the posframe.")

(defun flymake-posframe-hide-posframe ()
  "Hide messages currently being shown if any."
  (posframe-hide " *flymake-posframe-buffer*")
  (dolist (hook flymake-posframe-hide-posframe-hooks)
    (remove-hook hook #'flymake-posframe-hide-posframe t)))

(defun my/flymake-diagnostic-at-point-display-popup (text)
  "Display the flymake diagnostic TEXT inside a posframe."
  (posframe-show " *flymake-posframe-buffer*"
		 :string (concat flymake-diagnostic-at-point-error-prefix
				 (flymake--diag-text
				  (get-char-property (point) 'flymake-diagnostic)))
		 :position (point)
		 :foreground-color "cyan"
		 :internal-border-width 2
		 :internal-border-color "red"
		 :poshandler 'posframe-poshandler-window-bottom-left-corner)
  (dolist (hook flymake-posframe-hide-posframe-hooks)
    (add-hook hook #'flymake-posframe-hide-posframe nil t)))

(advice-add 'flymake-diagnostic-at-point-display-popup :override 'my/flymake-diagnostic-at-point-display-popup)

posframe4のソースコードのdocstringにしたがって:poshandlerを以下の13の選択肢のうちのどれかに変更すると様々な場所でflymakeのエラー表示ができるようになります。お好きな場所に設定してください。

:poshandler 'posframe-poshandler-window-bottom-left-corner

わたしがこのように設定している理由はポイントの近くに表示するとcompany-modeの補完と競合することがあるからです。

1.  `posframe-poshandler-frame-center'
2.  `posframe-poshandler-frame-top-center'
3.  `posframe-poshandler-frame-top-left-corner'
4.  `posframe-poshandler-frame-top-right-corner'
5.  `posframe-poshandler-frame-bottom-left-corner'
6.  `posframe-poshandler-frame-bottom-right-corner'
7.  `posframe-poshandler-window-center'
8.  `posframe-poshandler-window-top-left-corner'
9.  `posframe-poshandler-window-top-right-corner'
10. `posframe-poshandler-window-bottom-left-corner'
11. `posframe-poshandler-window-bottom-right-corner'
12. `posframe-poshandler-point-top-left-corner'
13. `posframe-poshandler-point-bottom-left-corner'

EditorConfig

プログラミング言語のインデントなどの設定をelispで書くのはやめましょう。 それぞれのプロジェクトのインデントに合わせるほうがよいのでEditorConfigEmacsパッケージをインストールします。

M-x package-install editorconfig
;; editorconfig
(editorconfig-mode 1)
(setq editorconfig-get-properties-function
      'editorconfig-core-get-properties-hash)

プロジェクトのルートディレクトリに以下のような.editorconfig5ファイルを置くとEmacsはこのルールでインデントするようになります。

# EditorConfig is awesome: https://EditorConfig.org

# top-most EditorConfig file
root = true

# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true

# Matches multiple files with brace expansion notation
# Set default charset
[*.{js,py}]
charset = utf-8

# 4 space indentation
[*.py]
indent_style = space
indent_size = 4

# Tab indentation (no size specified)
[Makefile]
indent_style = tab

# Indentation override for all JS under lib directory
[lib/**.js]
indent_style = space
indent_size = 2

# Matches the exact files either package.json or .travis.yml
[{package.json,.travis.yml}]
indent_style = space
indent_size = 2

  1. 銀の弾などない」とフレデリック・ブルックスが言っているのでどういう結果になるのかが楽しみですね。 ↩︎

  2. eglotはM-x eglot-shutdownでオフにすることができます。手動でオフにするのが面倒な場合でかつ、もし一つでもLanguage Server Protocolを使わないプロジェクトがあるならば、プロジェクトごとにeglotを使うかどうかの設定を追加すればいいでしょう。各プロジェクトに.dir-locals.elファイルを作りeglotをオフにする場合はremove-hookをeglotをオンにする場合add-hookを書けば良いと思います。すべてのプロジェクトでLanguage Server Protocolを使うならこの設定は不要です。 ↩︎

  3. M-.はポインタ上の名前で定義ジャンプします。M-,は最後のジャンプが行われた場所に戻ります。 M-?はポインタ上の名前への参照を見つけます。 ↩︎

  4. posframeを使うためにはEmacsのバージョンは26.0.91以上であることが必要です。 ↩︎

  5. ほとんどのエディタがEditorConfigに対応しています。 ↩︎

タグ一覧

お仕事のご相談などはこちらからどうぞ

お仕事の依頼はこちらからどうぞ