summaryrefslogtreecommitdiff
path: root/ledger-flymake.el
blob: 182efd7c8e05210232a9640702db93fa7ac78e26 (plain)
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
;;; ledger-flymake.el --- A ledger Flymake backend  -*- lexical-binding: t; -*-

;; Copyright (C) 2018 J. Alexander Branham (alex DOT branham AT gmail DOT com)

;; This file is not part of GNU Emacs.

;; This is free software; you can redistribute it and/or modify it under
;; the terms of the GNU General Public License as published by the Free
;; Software Foundation; either version 3, or (at your option) any later
;; version.
;;
;; This is distributed in the hope that it will be useful, but WITHOUT
;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
;; FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
;; for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING.  If not, write to the
;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
;; MA 02110-1301 USA.

;;; Commentary:
;; Flymake is the built-in Emacs package to support on-the-fly syntax checking.
;; This file adds support for flymake to `ledger-mode'.  Enable it by calling
;; `flymake-mode' from a file-visiting ledger buffer.

;;; Code:
(require 'cl-lib)
(require 'flymake)
(require 'ledger-exec)                  ; for `ledger-binary-path'

;; To silence byte compiler warnings in Emacs 25 and older:
(declare-function flymake-diag-region "flymake" (buffer line &optional col))
(declare-function flymake-make-diagnostic "flymake" (buffer beg end type text &optional data overlay-properties))

(defvar-local ledger--flymake-proc nil)

(defcustom ledger-flymake-be-pedantic nil
  "If non-nil, pass the --pedantic flag for ledger to the flymake backend.
If --pedantic is in your ledgerrc file, then --pedantic gets
passed regardless of the value."
  :type 'boolean
  :group 'ledger)

(defcustom ledger-flymake-be-explicit nil
  "If non-nil, pass the --explicit flag for ledger to the flymake backend.
If --explicit is in your ledgerrc file, then --explicit gets
passed regardless of the value."
  :type 'boolean
  :group 'ledger)

;; Based on the example from Flymake's info:
(defun ledger-flymake (report-fn &rest _args)
  "A Flymake backend for `ledger-mode'.

Flymake calls this with REPORT-FN as needed."
  (unless (executable-find ledger-binary-path)
    (error "Cannot find ledger"))
  ;; If a live process launched in an earlier check was found, that
  ;; process is killed.  When that process's sentinel eventually runs,
  ;; it will notice its obsoletion, since it have since reset
  ;; `ledger-flymake-proc' to a different value
  (when (process-live-p ledger--flymake-proc)
    (kill-process ledger--flymake-proc))
  ;; Save the current buffer, the narrowing restriction, remove any
  ;; narrowing restriction.
  (let ((source (current-buffer))
        (file (buffer-file-name)))
    (save-restriction
      (widen)
      ;; Reset the `ledger--flymake-proc' process to a new process
      ;; calling the ledger tool.
      (setq
       ledger--flymake-proc
       (make-process
        :name "ledger-flymake" :noquery t :connection-type 'pipe
        :buffer (generate-new-buffer " *ledger-flymake*")
        :command (cl-remove
                  nil
                  `(,ledger-binary-path "-f" ,file
                                        ,(when ledger-flymake-be-pedantic "--pedantic")
                                        ,(when ledger-flymake-be-explicit "--explicit")
                                        "balance"))
        :sentinel
        (lambda (proc _event)
          ;; Check that the process has indeed exited, as it might
          ;; be simply suspended.
          (when (eq 'exit (process-status proc))
            (unwind-protect
                ;; Only proceed if `proc' is the same as
                ;; `ledger--flymake-proc', which indicates that
                ;; `proc' is not an obsolete process.
                (if (with-current-buffer source (eq proc ledger--flymake-proc))
                    (with-current-buffer (process-buffer proc)
                      (goto-char (point-min))
                      ;; Parse the output buffer for diagnostic's
                      ;; messages and locations, collect them in a list
                      ;; of objects, and call `report-fn'.
                      (cl-loop
                       while (search-forward-regexp
                              ;; This regex needs to match the whole error.  We
                              ;; also need a capture group for the error message
                              ;; (that's group 1 here) and the line number
                              ;; (group 2).
                              (rx line-start "While parsing file \"" (one-or-more (not whitespace)) " line " (group-n 2 (one-or-more num)) ":\n"
                                  (zero-or-more line-start "While " (one-or-more not-newline) "\n" )
                                  (minimal-match (zero-or-more line-start (zero-or-more not-newline) "\n"))
                                  (group-n 1 "Error: " (one-or-more not-newline) "\n"))
                              nil t)
                       for msg = (match-string 1)
                       for (beg . end) = (flymake-diag-region
                                          source
                                          (string-to-number (match-string 2)))
                       for type = :error
                       collect (flymake-make-diagnostic source
                                                        beg
                                                        end
                                                        type
                                                        msg)
                       into diags
                       finally (funcall report-fn diags)))
                  (flymake-log :warning "Canceling obsolete check %s"
                               proc))
              ;; Cleanup the temporary buffer used to hold the
              ;; check's output.
              (kill-buffer (process-buffer proc))))))))))

;;;###autoload
(defun ledger-flymake-enable ()
  "Enable `flymake-mode' in `ledger-mode' buffers.

Don't enable flymake if flycheck is on and flycheck-ledger is
available."
  (unless (> emacs-major-version 25)
    (error "Ledger-flymake requires Emacs version 26 or higher"))
  ;; Add `ledger-flymake' to `flymake-diagnostic-functions' so that flymake can
  ;; work in ledger-mode:
  (add-hook 'flymake-diagnostic-functions 'ledger-flymake nil t)
  (flymake-mode))

(provide 'ledger-flymake)

;;; ledger-flymake.el ends here