-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathsly-asdf-flymake.el
312 lines (262 loc) · 14.2 KB
/
sly-asdf-flymake.el
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
;;; sly-asdf-flymake.el --- Flymake support for sly-asdf -*- lexical-binding: t; -*-
(require 'sly-asdf-flymake-base)
(require 'popup)
(require 'cl-lib)
(defvar sly-asdf-enable-experimental-syntax-checking nil)
(defvar sly-asdf--buffer-to-system (make-hash-table))
(defvar sly-asdf--system-to-buffers (make-hash-table))
(defvar sly-asdf--last-lisp-buffers nil)
(defvar *sly-asdf--last-point* nil)
(defvar *sly-asdf--flymake-backend-state* nil)
(defvar *sly-asdf--clobber-errors-with-bad-location* nil)
(defvar sly-asdf--after-oos-hook nil)
(defun sly-asdf-flymake ()
"Enable sly-asdf-flymake-base support."
;; MG: Some hacking was needed to get this to work. The main
;; issue here being that sly-asdf-flymake-base largely seems written with the assumption
;; of running and highlighting errors exclusively in the `current-buffer`.
;; Error state is buffer-local despite the error reporting format taking a buffer
;; However, in our case, we would like sly-asdf-flymake-base to be system aware, as well
;; as to continue to report errors if, for instance, we are in another buffer,
;; (e.g. the REPL)
(when sly-asdf-enable-experimental-syntax-checking
(sly-asdf-flymake-base-mode 1)
(setf *sly-asdf--flymake-backend-state* (make-hash-table))
;; Hack to supporting highlighting arbitary buffers
(advice-add 'sly-asdf-flymake-base--highlight-line :around 'sly-asdf--flymake-highlight-around-hook)
(add-hook 'sly-asdf--after-oos-hook 'sly-asdf--run-flymake-backend nil nil)
(add-hook 'after-change-functions 'sly-asdf--after-change-function nil nil)
(add-hook 'after-save-hook 'sly-asdf--after-save-function nil nil)
;(add-hook 'sly-event-hooks 'sly-asdf--before-event-function nil nil)
(run-with-idle-timer 0.5 t #'sly-asdf-show-popup)
;; MG: We cannot use `buffer-list-update-hook` as it is run before the buffer's underlying
;; file is visited (buffer-file-name) return nil. Use `post-command-hook` instead?.
(add-hook 'window-configuration-change-hook 'sly-asdf--flymake-buffer-list-update-hook)
(run-with-idle-timer 0.5 t #'sly-asdf-show-popup)
;; Initial setup
;; Remove any previous sly-asdf-flymake-base related fasls
(sly-asdf-remove-flymake-fasls)
;; Determine current systems and buffers
(sly-asdf--flymake-buffer-list-update-hook)
;; Run the backend
(sly-asdf--run-flymake-backend)))
(defun sly-asdf-remove-flymake-fasls ()
(interactive)
(sly-eval
`(slynk-asdf:remove-flymake-fasls)))
(defun sly-asdf--flymake-buffer-list-update-hook ()
(let* ((current-buffers (sly-asdf--current-lisp-buffers))
(new-buffers (cl-set-difference current-buffers sly-asdf--last-lisp-buffers))
(removed-buffers (cl-set-difference sly-asdf--last-lisp-buffers current-buffers)))
;; Update a map of buffers and their associated systems
(setf sly-asdf--last-lisp-buffers current-buffers)
;; For each new buffer, find it's corresponding system. Lisp buffers that do not
;; correspond to a system are marked as belonging to "orphan"
(cl-loop for buffer in new-buffers
for name = (buffer-name buffer)
for hash-value = (gethash name sly-asdf--buffer-to-system)
unless hash-value do
(setf (gethash name sly-asdf--buffer-to-system)
(or (sly-asdf-find-current-system buffer) "orphan")))
(cl-loop for buffer in removed-buffers
do (remhash buffer sly-asdf--buffer-to-system))
(setf sly-asdf--system-to-buffers
(sly-asdf--buffers-by-system))))
(defun sly-asdf--buffers-by-system ()
"Get a plist of current asdf systems and their associated buffers. "
(interactive)
(let ((buffers-by-system (make-hash-table :test #'equal)))
(cl-loop for buffer being the hash-keys of sly-asdf--buffer-to-system
using (hash-value system) do
(push buffer (gethash system buffers-by-system)))
buffers-by-system))
(defun sly-asdf--after-change-function (_start _stop _len)
"Start syntax check for current buffer if it isn't already running.")
;; MG: TODO - For this, we need to send unsaved buffers for compilation
;;(when (sly-asdf--lisp-buffer-p (current-buffer))
;;(sly-asdf--run-flymake-backend)))
;; MG: Let's not do this for now, flychecking the current image has side-effects
;; and is somewhat of a strange experience
;; (defun sly-asdf--before-event-function (&rest args)
;; "Fired before each sly event. Takes an ARGS, the first value of which is the operation."
;; (cl-destructuring-bind ((op &rest)) args
;; (when (eq op :channel-send)
;; ;;We may have evaluated some expression that may change
;; ;; the result of compilation, e.g., loading some other system
;; (sly-asdf--run-flymake-backend)))
;; nil)
(defun sly-asdf--after-save-function ()
"Start syntax check for current buffer if it isn't already running."
(sly-asdf--run-flymake-backend))
(defun sly-asdf--run-flymake-backend ()
"Flymake backend."
;;Override backend-state
(let ((sly-asdf-flymake-base--backend-state *sly-asdf--flymake-backend-state*))
(sly-asdf-flymake-base--with-backend-state 'sly-asdf-flymake-backend state
(let ((run-token (cl-gensym "backend-token")))
(setf (sly-asdf-flymake-base--backend-state-running state) run-token
(sly-asdf-flymake-base--backend-state-disabled state) nil
(sly-asdf-flymake-base--backend-state-diags state) nil
(sly-asdf-flymake-base--backend-state-reported-p state) nil)
(funcall 'sly-asdf-flymake-backend
(lambda (&rest args)
;;MG: Override backend-state, lexical scoping doesn't work here -> bc async?
(let ((sly-asdf-flymake-base--backend-state *sly-asdf--flymake-backend-state*))
(sly-asdf--remove-highlight-all-buffers)
(apply #'sly-asdf-flymake--handle-report 'sly-asdf-flymake-backend run-token args)
)))))))
(cl-defun sly-asdf-flymake--handle-report (backend token report-action
&key explanation force
&allow-other-keys)
"Handle reports from BACKEND identified by TOKEN.
BACKEND, REPORT-ACTION and EXPLANATION, and FORCE conform to the calling
convention described in `flymake-diagnostic-functions' (which
see). Optional FORCE says to handle a report even if TOKEN was
not expected."
(let* ((state (gethash backend sly-asdf-flymake-base--backend-state))
(first-report (not (sly-asdf-flymake-base--backend-state-reported-p state))))
(setf (sly-asdf-flymake-base--backend-state-reported-p state) t)
(let (expected-token
new-diags)
(cond
((null state)
(sly-asdf-flymake-base-error
"Unexpected report from unknown backend %s" backend))
((sly-asdf-flymake-base--backend-state-disabled state)
(sly-asdf-flymake-base-error
"Unexpected report from disabled backend %s" backend))
((progn
(setq expected-token (sly-asdf-flymake-base--backend-state-running state))
(null expected-token))
;; should never happen
(sly-asdf-flymake-base-error "Unexpected report from stopped backend %s" backend))
((not (or (eq expected-token token)
force))
(sly-asdf-flymake-base-error "Obsolete report from backend %s with explanation %s"
backend explanation))
((eq :panic report-action)
(sly-asdf-flymake-base--disable-backend backend explanation))
((not (listp report-action))
(sly-asdf-flymake-base--disable-backend backend
(format "Unknown action %S" report-action))
(sly-asdf-flymake-base-error "Expected report, but got unknown key %s" report-action))
(t
(setq new-diags report-action)
(save-restriction
(widen)
;; only delete overlays if this is the first report
;; (when first-report
;; (sly-asdf-flymake-base-delete-own-overlays
;; (lambda (ov)
;; (eq backend
;; (sly-asdf-flymake-base--diag-backend
;; (overlay-get ov 'sly-asdf-flymake-base-diagnostic)))))
;; )
(mapc (lambda (diag)
(sly-asdf-flymake-base--highlight-line diag)
(setf (sly-asdf-flymake-base--diag-backend diag) backend))
new-diags)
(setf (sly-asdf-flymake-base--backend-state-diags state)
(append new-diags (sly-asdf-flymake-base--backend-state-diags state)))
(when sly-asdf-flymake-base-check-start-time
(sly-asdf-flymake-base-log :debug "backend %s reported %d diagnostics in %.2f second(s)"
backend
(length new-diags)
(- (float-time) sly-asdf-flymake-base-check-start-time)))
(when (and (get-buffer (sly-asdf-flymake-base--diagnostics-buffer-name))
(get-buffer-window (sly-asdf-flymake-base--diagnostics-buffer-name))
(null (cl-set-difference (sly-asdf-flymake-base-running-backends)
(sly-asdf-flymake-base-reporting-backends))))
(sly-asdf-flymake-base-show-diagnostics-buffer))))))))
(cl-defun sly-asdf-flymake-backend (report-cb &rest _args)
"Flymake diagnostic function for sly-asdf. REPORT-FN required callback for sly-asdf-flymake-base."
(let ((systems (hash-table-keys (sly-asdf--buffers-by-system))))
;; Compile each system for which there exists a corresponding buffer
(cl-loop for system in systems
if (string-equal system "orphan") do
;; Orphaned buffers are compiled separately
(sly-asdf--compile-files-for-flymake (gethash system sly-asdf--system-to-buffers) report-cb)
else do
(sly-asdf--compile-system-for-flymake system report-cb))))
(defun sly-asdf--compile-files-for-flymake (filenames report-cb)
"Compile FILENAMES for Emacs, calling REPORT-CB with the result of compilation."
(sly-eval-async `(slynk-asdf:compile-files-for-flymake '(,@filenames))
(create-flymake-report-fn report-cb filenames)))
(defun sly-asdf--compile-system-for-flymake (system report-cb)
(let ((buffers (gethash system sly-asdf--system-to-buffers)))
(sly-eval-async `(slynk-asdf:compile-system-for-flymake ,system '(,@buffers))
(create-flymake-report-fn report-cb buffers))))
(defun create-flymake-report-fn (report-cb buffers)
(cl-flet ((remove-nulls (list) (cl-remove-if-not #'identity list)))
(lambda (result)
(sly-asdf--remove-highlight-from-buffers buffers)
(if result
(funcall report-cb
(remove-nulls
(mapcar (lambda (note)
(sly-asdf-note-to-diagnostic note))
(sly-compilation-result.notes result)))
:force t)
(funcall report-cb nil :force t)))))
(defun sly-asdf--parse-severity (severity)
(cl-case severity
((:warning :style-warning) :warning)
(t :error)))
(defun sly-asdf-note-to-diagnostic (note)
"Create a diagnostic for the given sly NOTE found in the buffer SOURCE."
(let* ((message (sly-note.message note))
(location (sly-note.location note))
(severity (sly-asdf--parse-severity (sly-note.severity note))))
;; Location may be an (:error) w/o a buffer, in which sly-location.buffer
;; returns a string (the error message)
(if (and location (listp (sly-location.buffer location)))
(progn
(let ((buffer (get-file-buffer (cadr (sly-location.buffer location))))
(pos (cadr (sly-location.position location ))))
(when buffer
(with-current-buffer buffer
(save-excursion
(goto-char pos)
(let ((bounds (or (sly-bounds-of-symbol-at-point)
(sly-bounds-of-sexp-at-point))))
(if bounds
(cl-destructuring-bind (start . end) bounds
(sly-asdf-flymake-base-make-diagnostic buffer start end severity message))
(sly-asdf-flymake-base-make-diagnostic buffer pos (+ pos 1) severity message))))))))
(unless *sly-asdf--clobber-errors-with-bad-location*
(progn
(sly-asdf-flymake-base-make-diagnostic (car (sly-asdf--current-lisp-buffers)) 1
(buffer-size (car (sly-asdf--current-lisp-buffers))) severity message))))))
(defun sly-asdf--remove-highlight (buffer)
"Remove sly-asdf-flymake-base overlays from target BUFFER."
(save-excursion
(with-current-buffer buffer
(sly-asdf-flymake-base-delete-own-overlays))))
(defun sly-asdf--remove-highlight-all-buffers ()
"Remove sly-asdf-flymake-base overlays from all Lisp buffers."
(cl-mapcar 'sly-asdf--remove-highlight (sly-asdf--current-lisp-buffers)))
(defun sly-asdf--remove-highlight-from-buffers (buffers)
"Remove sly-asdf-flymake-base overlays from all Lisp buffers."
(cl-mapcar 'sly-asdf--remove-highlight buffers))
(defun sly-asdf--flymake-highlight-around-hook (fun &rest args)
"Hook to apply around sly-asdf-flymake-base-highlight.
FUN is the original function and ARGS is a list containing
the diagnostic to highlight. Needed because sly-asdf-flymake-base-highlight does
not pass the diagnostic's buffer to `make-overlay`."
(let ((diagnostic (car args)))
(with-current-buffer (sly-asdf-flymake-base--diag-buffer diagnostic)
(apply fun args))))
(defun sly-asdf-show-popup ()
"Display a popup containing the diagnostic message at the current point."
(let ((point (point)))
(unless (equal point *sly-asdf--last-point*)
(setf *sly-asdf--last-point* point)
(let ((diags (sly-asdf-flymake-base-diagnostics point)))
(when diags
(popup-tip (sly-asdf-flymake-base-diagnostic-text (car diags)) :point point))))))
;; Activate (upon value of `sly-asdf-enable-experimental-syntax-checking')
(add-hook 'sly-connected-hook
;; MG: Investigate race, due to when ASDF loads?
;; OC: Is this still a problem after decoupling from sly-asdf?
(lambda () (run-with-idle-timer .5 nil #'sly-asdf-flymake)))
(provide 'sly-asdf-flymake)