我想在 Emacs 中用 Prettier。假设要格式化 hello.js
,可用命令:
$ <hello.js npx prettier --stdin-filepath hello.js
翻译成 Emacs Lisp:
(defun chunyang-prettier ()
"Prettier the current buffer."
(interactive)
(let ((buffer (generate-new-buffer " *prettier*")))
(pcase (call-process-region
nil nil "npx" nil buffer nil
"prettier" "--stdin-filepath" buffer-file-name)
(0
(replace-buffer-contents buffer 1)
(kill-buffer buffer))
(exit-code
(message "prettier failed with status %s" exit-code)
(display-buffer buffer)))))
这可以工作,但有一个问题是就是文件格式完美,一点点改动都不需要,上面的 replace-buffer-contents
还是会执行,浪费时间和CPU,我注意到 Prettier 有个 --check
选项,可以知道文件是否完美无需格式化,但是运行两次 Prettier 命令应该更慢,光 time npx prettier -v
就花了 0.2s。
这应该是所有格式化工具集成到 Emacs 都会遇到的问题,大家有没有了解是如何处理的?
先保存,然后用 npx prettier --write hello.js
直接格式化文件,最后在 Revert 貌似可行,真有这么操作的?
我也认为应该在 after-save-hook
,然后 auto-revert
。
计算格式化前后内容的hash 只要hash变了 就更新buffer
自己写一个 cli?
在 cli 里边 check,如果不一样就继续执行 format。这样应该比从 Emacs 调用两次更快一些。
2 个赞
wsug
6
瞄了一下源代码,这个 check 太简单粗暴了:
就是直接两个字符串对比啊。
我原本还想说,是不是可以给作者提意见,让它给 format 加一个返回码,表示文件没有改动。现在看来,整个过程应该是:先把输入文件 parse 到内存,然后重新按规则打印出来,所以它自己也不知道文件有没有改动,所以才需要对比输入和输出。
check 只不过是一次没有输出的 format 罢了。
因此完全可以自己实现一个 cli,在里边 format,然后检查输入和输出有无变化,增加一个返回码。
又看了一下 cli 的实现,它其实已经比较过输入输出有无变化了:
https://github.com/prettier/prettier/blob/master/src/cli/util.js#L494-L494
可以在下边这段里增加一个返回码?
https://github.com/prettier/prettier/blob/master/src/cli/util.js#L505-L518
用 emacs-prettier 会方便一点,直接执行 prettier-js
然后如果有改变,
我(一个 Node 小白)试了试,如果没改动,返回 100,有改动,打印结果,并返回 0:
#!/usr/bin/env node
const fs = require("fs");
const prettier = require("prettier");
const source = fs.readFileSync(0, "utf8");
const formatted = prettier.format(source, { parser: "babel" });
// 100 估计没被占用
if (source === formatted) process.exit(100);
process.stdout.write(formatted);
Emacs 设置跟上面差不多,就是多个检测 100 的环节
(defun chunyang-my-prettier ()
"Prettier the current buffer."
(interactive)
(let ((buffer (generate-new-buffer " *my-prettier*")))
(pcase (call-process-region
nil nil "my-prettier" nil buffer)
(100 (message "Already formatted"))
(0
(replace-buffer-contents buffer 1)
(kill-buffer buffer))
(exit-code
(message "prettier failed with status %s" exit-code)
(display-buffer buffer)))))
abcbc
11
可以用erase-buffer
和insert-buffer-substring
代替replace-buffer-contents
. insert-buffer-substring
是c函数,应该会好一些。
我自己的代码格式化实现是把格式化后的代码先放在临时的buffer里,和目标的buffer用compare-buffer-substrings
做比较。如果不一样,才替换目标buffer,并且把point放到同样的位置。这样buffer的修改状态和point也尽可能保留。
(defun format-buffer-with-exec (exec &rest opts)
"Format current buffer using external EXEC along with its
OPTS. It returns numeric status code of the command execution
result."
(let ((old-buf (current-buffer))
(fmt-buf "*Format Buffer Output*") status)
(save-restriction
(widen)
(with-current-buffer (get-buffer-create fmt-buf)
(let ((inhibit-read-only t))
(erase-buffer)
(message "Formatting buffer using %s..." exec)
(with-current-buffer old-buf
(setq status
(apply #'call-process-region
(append
(list nil nil exec nil fmt-buf nil) opts))))))
(if (zerop status)
(unwind-protect
(if (zerop (compare-buffer-substrings
old-buf nil nil fmt-buf nil nil))
(message "Already formatted")
(let ((p (point)))
(erase-buffer)
(insert-buffer-substring fmt-buf)
(goto-char p))
(message "Formatting buffer using %s...done" exec))
(let ((fmt-win (get-buffer-window fmt-buf)))
(if fmt-win (delete-window fmt-win))
(kill-buffer fmt-buf)))
(with-current-buffer fmt-buf
(let ((inhibit-read-only t))
(goto-char (point-min))
(insert (format "Command '%s %s'\n\n" exec (string-join opts " ")))
(special-mode)
(display-buffer fmt-buf)
(message "Failed to format buffer using %s" exec)))))
status))
(defun format-buffer-cmdline ()
"Return command line for external tool used to format code if
configured, or nil if not."
(cond ((memq major-mode (list 'c-mode 'c++-mode))
(list "astyle" "--style=java"
"--convert-tabs"
"--indent=spaces=4"
"--indent-col1-comments"
"--align-pointer=name"
"--align-reference=name"
"--attach-namespaces"
"--attach-classes"
"--attach-inlines"
"--attach-extern-c"
"--add-brackets"
"--pad-oper" "--pad-header" "--mode=c"))
((equal major-mode 'java-mode)
(list "astyle" "--style=java"
"--convert-tabs"
"--indent=spaces=4"
"--indent-col1-comments"
"--align-pointer=name"
"--align-reference=name"
"--attach-namespaces"
"--attach-classes"
"--attach-inlines"
"--attach-extern-c"
"--add-brackets"
"--pad-oper" "--pad-header" "--mode=java"))
((equal major-mode 'nxml-mode)
(list "tidy" "-indent" "-wrap" "0" "-omit" "-quiet" "-utf8"))
((equal major-mode 'python-mode)
(list "autopep8" "--max-line-length=100" "--aggressive" "-"))
((equal major-mode 'json-mode)
(list "jq" "--monochrome-output" "--indent" "2" "."))
((equal major-mode 'jsonnet-mode)
(list "jsonnetfmt" "-"))
((equal major-mode 'terraform-mode)
(list "terraform" "fmt" "-no-color" "-"))
(t nil)))
调用(format-buffer-with-exec (format-buffer-cmdline))
即可
这两种方法效果不完全等同,后者会保留 pointer marker overlay 等信息。
如果格式化得到的代码不变,这个函数费时间,需要一个字符一个字符比较。当然一般代码文件都不大,也没什么问题。
Prettier 有个 --cursor-offset
专门处理这个问题,但要输出 JSON,所以需要 Emacs 这里 Parse JSON,,我不考虑这个方法。