集成开发环境(IDE)是一种帮助程序员高效开发软件代码的软件应用程序。它通过将软件编辑、构建、测试和打包等功能结合到一个易于使用的应用程序中,提高了开发人员的工作效率。就像作家使用文本编辑器,会计师使用电子表格一样,软件开发人员使用 IDE 让他们的工作变得更轻松。当前比较流行的 Python IDEPyCharm, IDEA, VS CODE 等。 Emacs 作为一个可扩展的文本编辑器,处理各种文本数据是非常方便的。我们在 Emacs 里增加 elisp 扩展,就可以模拟各种 IDE 的环境,还能更好。

IDE 的功能

需要模拟的 IDE 基本功能应该包含:

  • 代码编辑
  • 语法高亮
  • 文档查找
  • 代码跳转
  • 语法解析
  • 代码规范
  • 代码检查
  • 代码调试

显示

基本的功能应该有:代码编辑和语法高亮。Emacs 里自带的 python-mode 就可以满足要求。当打开一个 .py 结尾的文件时,Emacs 会自动匹配 python-mode 模式。

更好的显示

如果觉得基本的 python-mode 不够,可以用更好的语法解析,然后显示。

 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 宏包处理。

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

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 代码是否符合语法,我们可以用专门的检查工具来判断。最常用的有 flycheckflymake 两种。我倾向于用 flycheck+ ,还是用 flymake 吧,Emacs 30 之后,就完全内 置了,对 ruff 的支持也非常好。

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

 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)            ; 实时检查
)

单独的检查函数

还可以单独定义一个检查函数,方便查看。

 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-, 返回原位置。

更全面的跳转

有时候,项目内的模块, elpyxref 都找不到,我们就自己定义一个函数,功过 egrep 来找到函数定义,然后手动打开

 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.

 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,它启动太慢了。

配置

 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 进行语法检查,

1
(add-hook 'eglot-managed-mode-hook #'flymake-ruff-load)

代码格式化

很多人写 python 代码,都不是很规范。由于 python 对格式有比较强的要求,不像 C/C++ 语言是自由格式。所以,我们可以用专门的工具来规范化代码。这里,仍然可以用 ruff 来完成此功能。

 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.