跳过正文

把 Emacs 打造为 Python IDE

·3104 字·7 分钟
目录

集成开发环境(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.

相关文章

文档中插入图形

·1593 字·4 分钟
传统的文字-图形融合 # 一般文档主要是进行字处理,而文字的书写、编辑、排版和画图是截然不同的两种模式。虽然在计算机里最后都是处理成二进制代码,但不管在哪种操作系统中,画图软件和字处理软件都是分属于不同的类别。早期的文档如果想同时包含文字和图形,只能把文字先打印出来,留出空白,再用画图软件做好图,把图形打印出来,用胶水粘贴上去。话说这么麻烦的方法,我当年是干过的,读研究生的时候,为了给一个会议期刊投稿,用 xvgr 画好了图,然后用胶水粘在文字稿的空白处。这种“精巧的活”我干起来非常吃力,看着文稿上黑乎乎的手指印,欲哭无泪,重复了好多次 ……。

如何创建个人 Blog

·1482 字·3 分钟
这里有三个概念首先需要理清: 用 org-mode 模式写文章,并将 org 格式的文章转换为 markdown 格式,以便 hugo 可以识别; 使用 hugo 建立静态网页; 将本地静态网页发布到互联网上。 org-mode 写文档 # org-mode 是一种标记模式,把文档的内容和格式分开,这样所有的文档都是文本模式。org-mode 模式已经被很多编辑器支持了,但还是 Emacs 支持得最好。因此,请参考 org mode 网站,学习如何使用 org-mode 模式。

人生的又一体验

·599 字·2 分钟
今天我对一个词有不同的体验了:飞来横祸。 周日的一大早,我就开车出去,准备和人谈事情。在西三环路上,我正听着吴军在得到上的新课程 —— 《教育的方法 50 讲》,心里还在感慨,这两年,我突然又对教育感兴趣,想再次教育辅导小朋友了。可是,满肚子的话,却又不知道从何说起了。这时,旁边道上的一个女孩突然急刹车,大概是快过了出口,准备变道出去吧。她正后方的一个男孩,车速过快、跟车太近,来不及刹车。又慌不择路,本能地打方向盘对着我就撞了过来。我的眼睛余光是看着一辆白色的汽车拦腰向我冲了过来,但我没有任何办法,车子直接被撞得飞了起来。我还来不及反应,空中的汽车又撞上了绿化带上的一根电线杆。我慢慢地看着空气气囊全部打开,听着车身被撞击得一声巨响。我甚至恍惚中看到车轮飞出去的弧线。嗯,这是不是“飞来横祸”?😜