集成开发环境(IDE)是一种帮助程序员高效开发软件代码的软件应用程序。它通过将软件编辑、构建、测试和打包等功能结合到一个易于使用的应用程序中,提高了开发人员的工作效率。就像作家使用文本编辑器,会计师使用电子表格一样,软件开发人员使用 IDE
让他们的工作变得更轻松。当前比较流行的 Python IDE
有 PyCharm
, IDEA
, VS CODE
等。 Emacs
作为一个可扩展的文本编辑器,处理各种文本数据是非常方便的。我们在 Emacs 里增加 elisp
扩展,就可以模拟各种 IDE
的环境,还能更好。
IDE 的功能
需要模拟的 IDE 基本功能应该包含:
代码编辑
语法高亮
文档查找
代码跳转
语法解析
代码规范
代码检查
代码调试
显示
基本的功能应该有:代码编辑和语法高亮。Emacs 里自带的 python-mode 就可以满足要求。当打开一个 .py
结尾的文件时,Emacs 会自动匹配 python-mode 模式。
更好的显示
如果觉得基本的 python-mode 不够,可以用更好的语法解析,然后显示。
Copy
1
2
3
4
5
6
7
8
9
10
11
12
(use-package tree-sitter
:ensure t
:hook ((python-mode . tree-sitter-hl-mode )
(python-ts-mode . tree-sitter-hl-mode )
)
)
(use-package tree-sitter-langs
:after tree-sitter
:config
(tree-sitter-require 'python )
)
有时候 Emacs 升级了之后, tree-sitter
版本不匹配,可以手动编译 tree-sitter-langs
,然后把编译好的 dylib
文件拷贝到 ~/.emacs.d/tree-sitter
目录下,取名为 libtree-sitter-python.dylib
设置好了之后,语法高亮就比较漂亮了。
文档说明
编程需要时时地查找某个函数的说明、用法等。我们采用 eldoc
宏包处理。
Copy
1
2
3
4
5
6
7
8
(use-package eldoc
:ensure t
:defer t
:init
(global-eldoc-mode )
)
(global-set-key (kbd "C-x c d" ) 'eldoc-doc-buffer )
当光标放在某个函数/变量上时,用快捷键 C-x c d
或者 M-x eldoc
就可以显示它的资料
说明—一般就是把函数定义里的说明信息显示出来。 比如,
更好的文档
eldoc
能够识别很多编程语言,既然通用性强,专业性就要稍稍弱一点。因此,我们可以用专门针对 python
语言的文档说明—elpy
。
Copy
1
2
3
4
5
(use-package elpy
:ensure t
:init
(elpy-enable )
)
当按下快捷键 C-c C-d
或者 M-x elpy-doc
时,就可以在 elpy
的 buffer 里显示文档。
比如,
elpy
不仅仅是 python 的文档调用,它包含了更多的 python 环境设置(后面我会讲到其他的应用),这里只是显示了它的文档显示功能。
语法检查
python 代码是否符合语法,我们可以用专门的检查工具来判断。最常用的有 flycheck
和
flymake
两种。我倾向于用 flycheck+ ,还是用 flymake
吧,Emacs 30 之后,就完全内
置了,对 ruff
的支持也非常好。
Copy
1
2
3
4
5
6
(use-package flymake
:ensure t
:custom ((flymake-start-on-flymake-mode nil )
(flymake-no-changes-timeout nil )
(setq flymake-show-diagnostics-at-end-of-line t )
(flymake-start-on-save-buffer t )))
如果发现代码里有红色的波浪线显示,就表示发现了语法错误,请尽快修改。
更好的语法检查
flymake
默认调用了 flake8
或者其他工具来进行语法检查,我们可以把这些工具换成最新最快的工具—- ruff
。
Copy
1
2
3
4
5
6
7
8
9
10
;; 通过 use-package 安装 flymake-ruff
(use-package flymake-ruff
:ensure t
:hook ((python-mode . flymake-ruff-load ) ; 传统 python-mode
(python-ts-mode . flymake-ruff-load )) ; Tree-sitter 版
:config
(setq flymake-ruff-program-args ' ("--quiet" "--output-format=concise" ))
(setq flymake-show-diagnostics-at-end-of-line t ) ; 行尾显示
(setq flymake-no-changes-timeout nil ) ; 实时检查
)
单独的检查函数
还可以单独定义一个检查函数,方便查看。
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
;; 定义ruff-check 修复命令
(reformatter-define ruff-check
:program "ruff"
:args ' ("check" "--fix" "-" )
:lighter " RuffCheck"
)
; 单独再定义一个检查函数
(defun ruff-check ()
(interactive )
(let ((current-file (buffer-file-name )))
(if current-file
(async-shell-command
(format "ruff check --select ALL %s" (shell-quote-argument current-file ))
)
)
)
)
;; 在python mode中,保存时自动运行ruff-format
(add-hook 'python-mode-hook
(lambda ()
(add-hook 'before-save-hook #' ruff-format-region nil t ))
)
ruff
需要单独安装,用 pip
或者 brew
都可以,只要系统的命令行可以找到它就行。
代码跳转
在书写/阅读代码时,常常需要从当前使用函数的位置,跳转到函数的定义位置。 elpy
就
直接支持这个功能 — 在函数名的位置处,按下快捷键 C-c .
或者 M-x elpy-goto-definition
或者 M-x elpy-goto-definition-other-window
(在新窗口打开定义文件),就可以跳转到函数的定义,即使定义是在另一个文档里也不要紧,
elpy
都自动打开那个文档,把光标放在函数的定义处。
按下 M-,
,就可以返回到原来的位置。
更好的跳转
我们可以用 xref
实现更好的跳转
C-x C-.
or M-x xref-find-definitins
C-x C-/
or M-x xref-find-references
然后用 C-x C-,
or M-x xref-pop-marker-stack
or M-,
返回原位置。
更全面的跳转
有时候,项目内的模块, elpy
和 xref
都找不到,我们就自己定义一个函数,功过 egrep
来找到函数定义,然后手动打开
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(defun elpy-goto-definition-or-rgrep ()
"Go to the definition of the symbol at point, if found. Otherwise, run `elpy-rgrep-symbol' ."
(interactive )
(if (version< emacs-version "25.1" )
(ring-insert find-tag-marker-ring (point-marker ))
(xref-push-marker-stack ))
(condition-case nil (elpy-goto-definition )
(error (elpy-rgrep-symbol
(concat "\\(def\\|class\\)\s" (thing-at-point 'symbol ) "(" )))))
(define-key elpy-mode-map (kbd "C-c ." ) 'elpy-goto-definition )
(define-key elpy-mode-map (kbd "C-c d" ) 'elpy-goto-definition-or-rgrep )
(define-key elpy-mode-map (kbd "C-x C-." ) 'xref-find-definitions )
(define-key elpy-mode-map (kbd "C-x C-/" ) 'xref-find-references )
(define-key elpy-mode-map (kbd "C-x C-," ) 'xref-pop-marker-stack )
当按下 C-c d
or M-x elpy-goto-definition-or-rgrep
时,就会把所有相关的 class 和 def 定义文件都找到。
代码补全
程序员通常都很懒,写代码的时候,如果能够只敲一两个字符,系统能够猜出来接下来要写什么就好了,直接按回车键或者 TAB,就把剩下的字符都自动补全了。Emacs 当然也有这样的插件 — company
:complete anything.
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
(use-package company
:ensure t
:init
(add-hook 'c-mode-common-hook 'company-mode )
(add-hook 'python-mode-common-hook 'company-mode )
:config
(eval-after-load 'c-mode ' (define-key c-mode-map (kbd "[tab]" ) 'company-complete ))
(setq company-backends ' ((company-capf
company-keywords
company-semantic
company-files
company-dabbrev
company-dabbrev-code
company-etags
company-clang
company-cmake
company-yasnippet )))
(setq company-tooltip-limit 20
company-tooltip-offset-display 'lines
company-tooltip-minimum 4
company-tooltip-flip-when-above t
company-tooltip-margin 3
company-tooltip-align-annotations t ; 对齐注释
company-tooltip-annotation-padding 1
company-text-face-extra-attributes ' (:weight bold :slant italic )
company-text-icons-add-background t
company-echo-delay 0
company-require-match nil
company-minimum-prefix-length 1 ; 只需敲 1 个字母就开始进行自动补全
company-show-numbers t ; 给选项编号 (按快捷键 M-1、M-2 等等来进行选择).
company-dabbrev-other-buffers t
company-dabbrev-ignore-case 'keep-prefix
company-selection-wrap-around t
company-show-quick-access 'left
company-idle-delay 0
company-tooltip-idle-delay 10
company-require-match nil
company-frontends ' (company-pseudo-tooltip-unless-just-one-frontend-with-delay
company-preview-frontend
company-echo-metadata-frontend )
company-backends ' (company-capf )
company-files-exclusions ' (".git/" ".DS_Store" )
)
(global-company-mode 1 )
)
可以看出, company
能识别很多种语言,python 当然也支持了。
LSP
LSP 是 Language Server Protocol 的缩写,该协议被用在编辑器或 IDE 与语言服务器之间,为编辑器提供自动补全,跳转到定义和引用查找等功能。以下内容来自 LSP 官网:
Adding features like auto complete, go to definition, or documentation on hover for a programming language takes significant effort. Traditionally this work had to be repeated for each development tool, as each tool provides different APIs for implementing the same feature.
A Language Server is meant to provide the language-specific smarts and communicate with development tools over a protocol that enables inter-process communication.
The idea behind the Language Server Protocol (LSP) is to standardize the protocol for how such servers and development tools communicate. This way, a single Language Server can be re-used in multiple development tools, which in turn can support multiple languages with minimal effort.
为一种编程语言添加自动补全,跳转到定义或悬停文档等功能需要很大的努力。在过去需要为每个开发工具重复这项工作,因为每个工具都提供不同的 API 来实现相同的功能。
语言服务器旨在提供特定语言的功能,并通过支持进程间通信的协议与开发工具进行通信。
语言服务协议(LSP)背后的想法是对此类服务器和开发工具如何通信的协议进行标准化。这样,单个语言服务器可以在多个开发工具中重复使用,从而可以以最小的努力支持多种语言。
这样,我们只需在 Emacs 里设置一个客户端,用来对接外部的 python 语言服务器程序,充分利用服务器的所有功能即可。
服务器
当前支持 python 最好的语法解析工具是微软开发的 pyright
,以及它的分支
basedpyright
。基本用法是一样的,后者支持更多的功能。它们都可以通过 pip
或者
brew
单独安装。
客户端
Emacs 里最流行的 LSP 客户端有三个:
lsp-mode (不要与 LSP 服务器混淆了)
eglot
lsp-bridge
其中,Emacs 内置了 Eglot,所以最轻量级,但基本满足了需求。除非有特殊的要求,不要轻易使用 lsp-mode,它启动太慢了。
配置
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
(use-package eglot
:ensure t
:defer t
:bind (:map eglot-mode-map
("C-c C-d" . elpy-doc )
("C-c C-e" . eglot-rename )
("C-c C-o" . python-sort-imports )
("C-c C-f" . eglot-format-buffer ))
:hook ((python-mode . eglot-ensure )
(python-mode . flyspell-prog-mode )
(python-mode . superword-mode )
(python-mode . hs-minor-mode )
(python-mode . (lambda () (set-fill-column 88 ))))
:config
;; 用basedpyright替换默认pylsp
;; 它会提供补全、跳转定义等功能,但诊断信息交给 flycheck
(add-to-list 'eglot-server-programs
' (python-mode . ("basedpyright-langserver" "--stdio" ))
)
;; 关闭 pyright 的 linting,由 ruff 处理
(setq eglot-workspace-configuration
' ((:python (:analysis (:typeCheckingMode . "basic" )
(:lintingMode . "off" )
(:reportMissingImports . true )))))
(setq eglot-ignored-server-capabilities
' (:diagnosticProvider )) ; 只让 Flymake 报告 lint
;; 日志级别(调试时用 'debug',日常用 'warning')
(setq eglot-log-level 'warning )
:init
(setq completion-category-overrides
' ((eglot (styles orderless ))))
)
;; 保存时自动格式化(使用 eglot 内建格式化)
(add-hook 'python-mode-hook
(lambda ()
(add-hook 'before-save-hook 'eglot-format-buffer nil t ))
)
;; 启动成功提示
(add-hook 'eglot-managed-mode-hook
(lambda ()
(message "[EGLOT] Connected to basedpyright" ))
)
可以看出,eglot 帮助 Emacs 对接了很多外部服务: basedpyrigh, elpy, flyspell
,
……。它接受 basedpyright
等处理 python 代码后发来的信息,然后交给相应的工具去
处理, eglot
再最后统一显示出来。比如,我们可以在调用 flymake
进行语法检查,
Copy
1
(add-hook 'eglot-managed-mode-hook #' flymake-ruff-load )
代码格式化
很多人写 python 代码,都不是很规范。由于 python 对格式有比较强的要求,不像 C/C++ 语言是自由格式。所以,我们可以用专门的工具来规范化代码。这里,仍然可以用 ruff
来完成此功能。
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
(use-package ruff-format
:ensure t
:after (flymake ruff )
)
(use-package reformatter
:ensure t
:hook
(python-mode . ruff-format-on-save-mode )
(python-ts-mode . ruff-format-on-save-mode )
:config
;; 定义ruff-format格式化命令
(reformatter-define ruff-format
:program "ruff"
:args ' ("format" "--stdin-filename" , buffer-file-name "-" )
:lighter " UrffFmt"
)
)
(defcustom ruff-format-import-command "ruff"
"Ruff command to use for formatting."
:type 'string
:group 'ruff-format-import )
;;;###autoload (autoload 'ruff-format-import-buffer "ruff-format-import" nil t)
;;;###autoload (autoload 'ruff-format-import-region "ruff-format-import" nil t)
;;;###autoload (autoload 'ruff-format-import-on-save-mode "ruff-format-import" nil t)
(reformatter-define ruff-format-import
:program ruff-format-import-command
:args (list "check" "--fix" "--select=I" "--stdin-filename" (or (buffer-file-name ) input-file ))
:lighter " RuffFmt"
:group 'ruff-format-import )
文件保存时,代码会自动被格式化。
至此,我们已经打造了一个漂亮好用的 Python IDE。Enjoy it.