aboutsummaryrefslogtreecommitdiff
path: root/tw/services/files/emacs-packages/flymake-guile.el
diff options
context:
space:
mode:
authorTimo Wilken2024-03-09 14:52:56 +0100
committerTimo Wilken2024-03-10 16:19:00 +0100
commitde20fc8d904643ffe6957febfc6a24e57c12b512 (patch)
tree8177459e40786bd432a37c5833f26350fb689356 /tw/services/files/emacs-packages/flymake-guile.el
parentda5e9d5ee98dfc216eb7e3b1559c09f4bf868bf6 (diff)
Separate home service into PIM, dev env and graphical parts
This means we only instantiate Shepherd and mcron services if we really need them, to avoid annoyance on servers.
Diffstat (limited to 'tw/services/files/emacs-packages/flymake-guile.el')
-rw-r--r--tw/services/files/emacs-packages/flymake-guile.el123
1 files changed, 123 insertions, 0 deletions
diff --git a/tw/services/files/emacs-packages/flymake-guile.el b/tw/services/files/emacs-packages/flymake-guile.el
new file mode 100644
index 00000000..edfbce82
--- /dev/null
+++ b/tw/services/files/emacs-packages/flymake-guile.el
@@ -0,0 +1,123 @@
+;;; flymake-guile.el --- Flymake checker using `guild compile' -*- lexical-binding: t -*-
+;;; Commentary:
+;;; "guild compile" compiles Guile code to bytecode and can output a few basic
+;;; warnings. Let's use this as a linter!
+;;; Code:
+
+(require 'custom)
+(require 'flymake)
+(require 'geiser-impl) ; for `geiser-active-implementations'
+
+(defcustom flymake-guile-guild-executable "guild"
+ "The guild executable to use. This will be looked up in $PATH."
+ :type '(string)
+ :risky t
+ :group 'flymake-guile)
+
+(defvar-local flymake-guile--flymake-proc nil
+ "The latest invocation of guild compile.")
+
+(defvar-local flymake-guile--temp-file nil
+ "The temporary file name to pass to guild.")
+
+(defun flymake-guile--encode-filename (buffer-name)
+ "Create a safe temporary file name from BUFFER-NAME."
+ (concat "/tmp/flymake-guile-"
+ (string-replace
+ "/" "!" ; we don't want to create subdirs under /tmp
+ (or buffer-name
+ (format "temp-%s.scm"
+ (random most-positive-fixnum))))))
+
+;; See info node: (flymake)An annotated example backend.
+(defun flymake-guile (report-fn &rest _args)
+ "Run guild compile and report diagnostics from it using REPORT-FN.
+Any running invocations are killed before running another one."
+ (unless (executable-find flymake-guile-guild-executable)
+ (funcall report-fn :panic
+ :explanation "Cannot find `flymake-guile-guild-executable' program")
+ (error "Cannot find guild executable"))
+
+ (unless flymake-guile--temp-file
+ (setq-local flymake-guile--temp-file (flymake-guile--encode-filename (buffer-file-name))))
+
+ ;; Kill previous check, if it's still running.
+ (when (process-live-p flymake-guile--flymake-proc)
+ (kill-process flymake-guile--flymake-proc))
+
+ ;; This needs `lexical-binding'.
+ (let ((source (current-buffer))
+ ;; Copy `flymake-guile--temp-file' to a local var so that we can refer to it in the `lambda' below.
+ (temp-file flymake-guile--temp-file))
+ (save-restriction
+ (widen)
+ ;; Send the buffer to guild on stdin.
+ (with-temp-file flymake-guile--temp-file
+ (insert-buffer-substring-no-properties source))
+ (setq flymake-guile--flymake-proc
+ (make-process
+ :name "flymake-guild" :noquery t :connection-type 'pipe
+ ;; Direct output to a temporary buffer.
+ :buffer (generate-new-buffer " *flymake-guile*")
+ ;; Guild can't read from stdin; it needs a file.
+ :command (list flymake-guile-guild-executable "compile"
+ ;; See "guild --warn=help" for details.
+ ;; "--warn=unsupported-warning" ; ignore unsupported warning types
+ ;; "--warn=unused-variable" ; too many false positives from macros
+ "--warn=unused-toplevel"
+ "--warn=shadowed-toplevel"
+ "--warn=unbound-variable"
+ "--warn=macro-use-before-definition"
+ "--warn=use-before-definition"
+ "--warn=non-idempotent-definition"
+ "--warn=arity-mismatch"
+ "--warn=duplicate-case-datum"
+ "--warn=bad-case-datum"
+ "--warn=format"
+ "-L" (expand-file-name
+ (project-root (project-current nil (file-name-directory
+ (buffer-file-name source)))))
+ flymake-guile--temp-file)
+ :sentinel
+ (lambda (proc _event)
+ "Parse diagnostic messages once the process PROC has exited."
+ ;; Check the process has actually exited, not just been suspended.
+ (when (memq (process-status proc) '(exit signal))
+ (unwind-protect
+ ;; Only proceed if we've got the "latest" process.
+ (if (with-current-buffer source (not (eq proc flymake-guile--flymake-proc)))
+ (flymake-log :warning "Canceling obsolete check %s" proc)
+ (with-current-buffer (process-buffer proc)
+ (goto-char (point-min))
+ (cl-loop
+ with msg-regexp = (rx bol (literal temp-file) ":" ; filename
+ (group (+ digit)) ":" ; line
+ (group (+ digit)) ": " ; column
+ (group (or "warning" "error")) ": " ; type
+ (group (+ not-newline)) eol) ; message
+ while (search-forward-regexp msg-regexp nil t)
+ for (beg . end) = (flymake-diag-region
+ source ; we filter for messages matching our buffer in the regexp
+ (string-to-number (match-string 1))
+ ;; guild outputs 0-based column numbers
+ (1+ (string-to-number (match-string 2))))
+ for type = (pcase (match-string 3)
+ ("warning" :warning)
+ ("error" :error)
+ (type (error "Unknown guild error type %s" type)))
+ collect (flymake-make-diagnostic source beg end type (match-string 4))
+ into diags
+ finally (funcall report-fn diags))))
+ ;; Clean up temporary buffer.
+ (kill-buffer (process-buffer proc))
+ (delete-file temp-file)))))))))
+
+(defun flymake-guile-enable ()
+ "Set up the Guile checker for flymake, if in a Guile buffer."
+ (when (memq 'guile geiser-active-implementations)
+ (add-hook 'flymake-diagnostic-functions #'flymake-guile nil t)))
+
+(add-hook 'scheme-mode-hook #'flymake-guile-enable)
+
+(provide 'flymake-guile)
+;;; flymake-guile.el ends here