| git.druid.rocks | index | druid520 | radium | radium-checkonsave.el |
radium-checkonsave.el
;;; radium-checkonsave.el, on-save syntax check without lsp -*- lexical-binding: t; -*-
;;
;; silently syntax-checks whatever i just saved into a background
;; compilation buffer, reporting only pass/fail in the echo area - no
;; lsp. if it fails i pop the buffer so the errors are clickable /
;; jumpable the normal compilation-mode way (M-g n / M-g p). each file
;; gets its own *checkonsave:<file>* buffer, and a re-save before the
;; last check finished kills the old process so they never race.
;;
;; the compile column is set so gcc's "file:line:col: error:" clicks
;; land on the right spot; %s in each command is the shell-quoted path.
(defvar radium-checkonsave-alist
'((c-mode . "gcc -fsyntax-only -x c -Wall -Wextra %s")
(nsc-mode . "nscc %s -o /dev/null")
(ada-mode . "gnatmake -q -gnatc %s")
(asm-mode . "as -o /dev/null %s")
(cperl-mode . "perl -c %s")
(sh-mode . "sh -n %s")
(emacs-lisp-mode . "emacs --batch -Q --eval '(kill-emacs (if (byte-compile-file %S) 0 1))'")
(python-mode . "python3 -c 'import ast,sys; ast.parse(open(sys.argv[1]).read(), sys.argv[1])' %s"))
"major mode -> shell command template (%s = shell-quoted file name)
run in the background after every save. gcc gets -x c so .h headers
are checked as plain c, not guessed; nscc needs no such flag - a .nsh
compiles standalone exactly like a .nsc, confirmed directly against
real kaboom source (a header of bare declarations, like syscalls.nsh,
and a real .nsc both exit 0). languages with no one-shot syntax
checker on netbsd (scheme, forth, kona, cron) just aren't listed.")
(defun radium-checkonsave ()
(let ((cmd (cdr (assq major-mode radium-checkonsave-alist))))
(when (and cmd buffer-file-name
(executable-find (car (split-string cmd))))
(let* ((file buffer-file-name)
(base (file-name-nondirectory file))
;; %S in a template (only emacs-lisp-mode's) means "format
;; this as a real elisp string literal" - format's own %S
;; already does that (prin1-to-string) itself, so passing
;; file raw here is correct; pre-stringifying it first (as
;; this used to) double-quotes it, e.g. (byte-compile-file
;; "\"/x.el\""), which is not a valid path and always
;; failed - confirmed by running the built command.
(arg (if (string-match "%S" cmd)
file
(shell-quote-argument file)))
(full (format cmd arg))
(buf (get-buffer-create (format "*checkonsave:%s*" base))))
(with-current-buffer buf
(let ((inhibit-read-only t))
(erase-buffer)
(unless (derived-mode-p 'compilation-mode)
(compilation-mode)))
(setq default-directory (file-name-directory file)))
;; one buffer per file, and only one live check per file - a
;; save right after another save kills the stale run first.
(when-let ((old (get-buffer-process buf)))
(delete-process old))
(make-process
:name (format "checkonsave:%s" base)
:buffer buf
:command (list "/bin/sh" "-c" full)
:sentinel
(lambda (_proc event)
(when (and (string-match-p "finished\\|exited" event)
(buffer-live-p buf))
(let ((ok (string-prefix-p "finished" event)))
(with-current-buffer buf
(goto-char (point-min)))
(if ok
(message "ok: %s syntax check." base)
(message "err: %s syntax check - %s to jump."
base
(substitute-command-keys "\\`M-g n'"))
(display-buffer buf))))))))))
(add-hook 'after-save-hook #'radium-checkonsave)
(provide 'radium-checkonsave)