EmacsでのlspをベースとしたPython開発環境
この記事はEmacs Advent Calendar 2021の2日目の記事です。
こんにちは。 昨年のEmacs Advent Calendar 2020の私の記事では、EmacsでのElpyを用いたPython開発環境を紹介しました。
しかし、この一年でlanguage server protocol(lsp)に入門し、Elpyからlspでの開発環境に移行しました。 移行した理由は、language server protocolを触ってみたかったという興味本位です。
この記事では私のEmacsでのlspをベースとしたPython開発環境を紹介します。
昨年の記事と被る部分も多くありますが、この記事だけでEmacsの設定を完結できるように重複部分も記載しておきます。
少しでも読んだ方の参考になると嬉しいです。
Pythonの開発環境
まず、Python自体の開発環境は、pyenv、pipx、poetryを用いて構築します。 これらを用いることで、プロジェクトごとに開発環境を分離しています。
具体的な構築方法は、Python開発環境(pyenv+pipx+poetry)の構築(2020)に記載していますので、よろしければご参照ください。
Emacsでの開発環境
では、本題のEmacsでの開発環境です。 PythonをEmacsで快適に書くために、以下のEmacsパッケージを導入してます。
Emacsパッケージ | 概要 |
---|---|
smartparens | 括弧等のペアの処理 |
blacken | コードフォーマッタ |
highlight-indent-guides | インデントの強調表示 |
rainbow-delimiters | 対応する括弧の強調表示 |
whitespace-mode | 空白の可視化 |
mwim | 行頭・行末移動の拡張 |
py-isort | pythonファイルのimport順の整理 |
company-mode | Emacsの入力補完用パッケージ |
yasnippet | テンプレートの挿入 |
Flycheck | 構文チェッカー |
lsp-mode | lspを利用するためのクライアントパッケージ |
lsp-pyright | pyrightをlsp-modeで利用 するためのパッケージ |
では、具体的に各パッケージの説明と設定を紹介します。
なお、私はPackage管理のパッケージとして、leaf.elを使用していますので、今回はleaf.elの記法で設定していきます。 個人的にはleaf.elは可読性が高く、設定もしやすいので、とてもありがたいです。
smartparens
smartparensは、括弧等のペアを処理するためのマイナーモードです。 以下のようにデフォルト設定を有効化すると、「モードに合った括弧の自動補完」や「選択範囲を括弧等のペアでラップ」等ができるようになります。 色々機能があるので、詳細は公式サイトをご覧ください。
(leaf smartparens
:ensure t
:hook (after-init-hook . smartparens-global-strict-mode) ; strictモードを有効化
:require smartparens-config
:custom ((electric-pair-mode . nil))) ; electirc-pair-modeを無効化
また、私は、上記でstrictモードを有効化して、括弧等のペアが崩れないように強制しています。
例えば、strictモードを有効にすると、以下のようにhogeの末尾でC-k
を押下した場合、hogeの末尾以降がすべて削除されずに「)」が残り、括弧のペアを維持してくれます。便利!
(hoge huga)
↓
(hoge)
blacken
blackenは、外部のblackを利用してアクティブなバッファをコードフォマットに従って整形するパッケージです。 blackはかなり厳格なコードフォーマットを提供しています。
blackenはblackが必要となりますので、別途インストールします。
blackはpipでインストールできますので、globalのpyhtonにインストールするか、pipでインストールするコマンドの管理を参考にpipxでインストールしてください。おすすめは管理がしやすいpipxでのインストールです。
以下の設定でblackenを有効化します。 私は、blackの デフォルト設定から文字制限や一部の変換強制を緩和しています。
(leaf blacken
:ensure t
:custom ((blacken-line-length . 119) ; 1行の流さを119文字まで許可
(blacken-skip-string-normalization . t))) ; 文字リテラルの「'」を「"」に変更しないように抑制
コードフォーマットにより整形を実施したい時は、M-x blacken-buffer
でアクティブなバッファを整形します。
highlight-indent-guides
highlight-indent-guidesは、インデントの位置を強調表示するマイナーモードです。
どのように強調表示されるかは公式サイトのスクリーンショットをご覧ください。 特にPythonはindent位置が重要になりますので、重宝しています。
(leaf highlight-indent-guides
:ensure t
:blackout t
:hook (((prog-mode-hook yaml-mode-hook) . highlight-indent-guides-mode))
:custom (
(highlight-indent-guides-method . 'character)
(highlight-indent-guides-auto-enabled . t)
(highlight-indent-guides-responsive . t)
(highlight-indent-guides-character . ?\|)))
rainbow-delimiters
rainbow-delimitersは、対応する括弧を強調表示するマイナーモードです。 以下の設定をすることで、対応する括弧を色分けして表示できますので、対応関係を視認しやすくなります。
(leaf rainbow-delimiters
:ensure t
:hook
((prog-mode-hook . rainbow-delimiters-mode)))
white-space-mode
white-space-modeは、スペースやタブなどの空白を表示するマイナーモードです。また、不要なスペースやタブの削除も可能です。
設定は@itiut@github様のqiita記事を参考に以下のように設定しています。
C-c W
で不要なスペースやタブを明示的に除去するようにしています。
(leaf whitespace
:ensure t
:commands whitespace-mode
:bind ("C-c W" . whitespace-cleanup)
:custom ((whitespace-style . '(face
trailing
tabs
spaces
empty
space-mark
tab-mark))
(whitespace-display-mappings . '((space-mark ?\u3000 [?\u25a1])
(tab-mark ?\t [?\u00BB ?\t] [?\\ ?\t])))
(whitespace-space-regexp . "\\(\u3000+\\)")
(whitespace-global-modes . '(emacs-lisp-mode shell-script-mode sh-mode python-mode org-mode))
(global-whitespace-mode . t))
:config
(set-face-attribute 'whitespace-trailing nil
:background "Black"
:foreground "DeepPink"
:underline t)
(set-face-attribute 'whitespace-tab nil
:background "Black"
:foreground "LightSkyBlue"
:underline t)
(set-face-attribute 'whitespace-space nil
:background "Black"
:foreground "GreenYellow"
:weight 'bold)
(set-face-attribute 'whitespace-empty nil
:background "Black")
)
mwim
mwimは、コードの先頭・末尾への移動と行頭・行末への移動を行うコマンドを提供します。
実際の動作は公式サイトのスクリーンショットをご参照ください。
私はC-a
とC-e
に割り当てて、行頭・行末への移動を拡張しています。
(leaf mwim
:ensure t
:bind (("C-a" . mwim-beginning-of-code-or-line)
("C-e" . mwim-end-of-code-or-line)))
py-isort
py-isortは、外部のisortを利用してアクティブなバッファ内のimport順を整理するパッケージです。
isortはimportするライブラリの順序をPEP8で定義される以下のルールに従ってソートします。
import文 は次の順番でグループ化すべきです:
- 標準ライブラリ
- サードパーティに関連するもの
- ローカルな アプリケーション/ライブラリ に特有のもの
上のグループそれぞれの間には、1行空白を置くべきです。
py-isortはisortが必要となりますので、別途インストールします。
isortはpipでインストールできますので、globalのpyhtonにインストールか、pipでインストールするコマンドの管理を参考にpipxでインストールしてください。おすすめは管理がしやすいpipxでのインストールです。
Emacsでは、以下の設定でpy-isortを有効化します。
(leaf py-isort :ensure t)
before-save-hook
にpy-isort-before-save
フックする設定を追加することで、バッファの保存時に自動でソートするようにもできます。
しかし、私は自分でソートタイミングを指定したいので、設定せずにM-x py-isort-buffer
で、明示的にソートしています。
company-mode
company-modeは、Emacsの入力補完用のパッケージです。 python以外でも色々な言語等で入力補完ができます。
私は以下の設定で、1文字入力以降から補完を開始して、C-p
、C-n
, C-i
等で補完候補を選択できるようにしています。
(leaf company
:ensure t
:leaf-defer nil
:blackout company-mode
:bind ((company-active-map
("M-n" . nil)
("M-p" . nil)
("C-s" . company-filter-candidates)
("C-n" . company-select-next)
("C-p" . company-select-previous)
("C-i" . company-complete-selection))
(company-search-map
("C-n" . company-select-next)
("C-p" . company-select-previous)))
:custom ((company-tooltip-limit . 12)
(company-idle-delay . 0) ;; 補完の遅延なし
(company-minimum-prefix-length . 1) ;; 1文字から補完開始
(company-transformers . '(company-sort-by-occurrence))
(global-company-mode . t)
(company-selection-wrap-around . t))
yasnippet
yasnippetは、テンプレートの挿入を提供しているパッケージです。
こちらもPython以外の言語でも使えます。
Pythonのテンプレートは、yasnippet-snippetsのpython-modeを参照ください。
例えば、ifm
と入力してC-i
を押下すると、if __name__ == '__main__':
が展開されます。
以下の設定では、compay-modeの補完対象として、yasnippetのテンプレートも表示されるようになっています。company-modeと組み合わせることで一つのUIで入力補完とテンプレート挿入ができるようになり、とても便利です。
(leaf yasnippet
:ensure t
:blackout yas-minor-mode
:custom ((yas-indent-line . 'fixed)
(yas-global-mode . t)
)
:bind ((yas-keymap
("<tab>" . nil)) ; conflict with company
(yas-minor-mode-map
("C-c y i" . yas-insert-snippet)
("C-c y n" . yas-new-snippet)
("C-c y v" . yas-visit-snippet-file)
("C-c y l" . yas-describe-tables)
("C-c y g" . yas-reload-all)))
:config
(leaf yasnippet-snippets :ensure t)
(leaf yatemplate
:ensure t
:config
(yatemplate-fill-alist))
(defvar company-mode/enable-yas t
"Enable yasnippet for all backends.")
(defun company-mode/backend-with-yas (backend)
(if (or (not company-mode/enable-yas) (and (listp backend) (member 'company-yasnippet backend)))
backend
(append (if (consp backend) backend (list backend))
'(:with company-yasnippet))))
(defun set-yas-as-company-backend ()
(setq company-backends (mapcar #'company-mode/backend-with-yas company-backends))
)
:hook
((company-mode-hook . set-yas-as-company-backend))
))
Flycheck
Flycheckは、構文チェッカーです。
(leaf flycheck
:ensure t
:hook (prog-mode-hook . flycheck-mode)
:custom ((flycheck-display-errors-delay . 0.3)
(flycheck-indication-mode . 'left-margin)) ;terminalで使うので、fringeではなくmarginに警告を表示
:config (add-hook 'flycheck-mode-hook #'flycheck-set-indication-mode) ; flycheckのみでmarginを使用
(leaf flycheck-inline
:ensure t
:hook (flycheck-mode-hook . flycheck-inline-mode)))
また、Flycheckの拡張として、以下を導入しています。
- flycheck-inline: エラーをエコーエリアではなく、インライン内に表示
lsp-mode
lsp-modeは、Emacsでlspを利用できるようにするためのパッケージです。 lsp-modeを導入することで、これまで設定してきた他のパッケージ(companyやflycheck等)を上手く統合してEmacsがIDEのように動作します。
lspを利用するためのパッケージはいくつかありますが、私はlsp-modeとlsp-uiを利用しています。 lsp-modeではlsp自体の設定、lsp-uiでlsp-modeにおけるuiの設定ができます。
細かく設定できますので、公式サイトを参考に設定して、自分好みの設定をしてみてください。 参考までに、私の設定を以下に記載しておきます。
(leaf lsp-mode
:ensure t
:commands (lsp lsp-deferred)
:config
:custom ((lsp-keymap-prefix . "C-c l")
(lsp-log-io . t)
(lsp-keep-workspace-alive . nil)
(lsp-document-sync-method . 2)
(lsp-response-timeout . 5)
(lsp-enable-file-watchers . nil))
:hook (lsp-mode-hook . lsp-headerline-breadcrumb-mode)
:init (leaf lsp-ui
:ensure t
:after lsp-mode
:custom ((lsp-ui-doc-enable . t)
(lsp-ui-doc-position . 'at-point)
(lsp-ui-doc-header . t)
(lsp-ui-doc-include-signature . t)
(lsp-ui-doc-max-width . 150)
(lsp-ui-doc-max-height . 30)
(lsp-ui-doc-use-childframe . nil)
(lsp-ui-doc-use-webkit . nil)
(lsp-ui-peek-enable . t)
(lsp-ui-peek-peek-height . 20)
(lsp-ui-peek-list-width . 50))
:bind ((lsp-ui-mode-map ([remap xref-find-definitions] .
lsp-ui-peek-find-definitions)
([remap xref-find-references] .
lsp-ui-peek-find-references))
(lsp-mode-map ("C-c s" . lsp-ui-sideline-mode)
("C-c d" . lsp-ui-doc-mode)))
:hook ((lsp-mode-hook . lsp-ui-mode))))
lsp-pyright
lspを利用するためには、肝心なlanguage serverが必要となります。 Python向けのlanguage serverの選択肢は、有名所として以下のものがあります。
- pyright(インストールにnodejsが必要)
- python-lsp-server(Jediが必要)
以前使っていた開発環境のElpyはJediベースなので、今回は毛色が異なるpyrightを利用します。 また、lsp-pyright(lsp-modeでpyrightを利用できるようにするパッケージ)もインストールします。
pyrightのインストール
まず、nodejsが必要となるので、nodejsをインストールしておきます。 nodejsのインストール方法は色々な記事があると思いますので、そちらを参照いただければと思います。 私もM1 Macでのnodejsのインストール記事を書いていますので、よろしければ参考にしてください。
nodejsがインストールされていれば、以下コマンドでpyrightをインストールします。
npm install -g pyright
lsp-pyrightの設定
以下の設定より、python-mode時にlsp-pyrightを有効化します。
(leaf lsp-pyright
:ensure t
:hook (python-mode-hook . (lambda ()
(require 'lsp-pyright)
(lsp-deferred))))
このpython開発環境でできること
以上で、必要なパッケージのインストールと設定が完了しました。 ここまでインストールした各パッケージには色々な機能がありますが、私がよく使う機能/コマンドを少しだけ紹介します。
-
テンプレート yasnippetでテンプレートを挿入しています。
if __name__ == '__main__'
はよく忘れるので、ifm
でテンプレート挿入できるので、便利です。 -
自動補完 lspがcompany-modeのバックエンドとして稼動します。 company-modeの設定で1文字から補完を開始し、yasnippetのテンプレートも補完対象としています。
-
定義ジャンプ
-
M-. (xref-find-definitions)
カーソル上のシンボル(クラス名や関数名等)の定義先に移動します。
-
M-? (xref-find-references)
カーソル上のシンボルを呼び出している箇所の候補を表示して、選択後に移動します。 -
M-, (xref-pop-marker-stack)
M-.
やM-?
で移動した先から移動前の場所に戻ります。
-
-
構文チェック flycheckで動的に構文がチェックされます。
-
リファクタリング
-
M-x py-isort-buffer
アクティブなバッファ内のimport順を整理します。
-
M-x blacken-buffer
アクティブなバッファ内にblackのコードフォーマットを適用します。 -
C-c W(whitespace-cleanup)
アクティブなバッファ内の不要なスペースやタブを明示的に除去します。
-
終わりに
ここまで、読んでいただき、ありがとうございます。 これで一通りのEmacsでのlspをベースとしたPython開発環境を構築できました。 今回の設定を含む私のEmacsの設定がありますので、よろしければ参考にしてください。
今回の設定は、他の統合開発環境ならば標準機能であったり設定が簡単そうだからそっちを使ったほうが良いかなと思うこともあります。
しかし、それでも私がEmacsを使う理由は、自由にカスタマイズできるEmacsが大好きだからです 。
今回であれば、自分の好きなように動作する統合開発環境が作れました。 Emacsではelispが読めれば、自分が改善したいところはかなり改善できますし、今回紹介しましたように色々な方が便利なパッケージを公開してくれています。 カスタマイズが止められない、それがEmacs。
この記事で少しでもEmacsユーザが増え、Emacs界?の盛り上がりに貢献できると嬉しいです。
すべてのEmacsユーザに幸あれ!
謝辞
leaf.elや今回紹介させていただいたパッケージ及び紹介できなったパッケージの作者様、設定ファイルを公開してくださっている方々、いつもありがとうございます。
今回の設定は、色々な方々の設定ファイルを参考にさせていただいている箇所もあるのですが、出典を記録していないため、参考元のサイトが不明となっており、申し訳ございません。
不都合等ありましたら、コメント等でご連絡いただけますと幸いです。