diff options
author | Yuuki Harano <masm+github@masm11.me> | 2021-11-11 00:39:53 +0900 |
---|---|---|
committer | Yuuki Harano <masm+github@masm11.me> | 2021-11-11 00:39:53 +0900 |
commit | 4dd1f56f29fc598a8339a345c2f8945250600602 (patch) | |
tree | af341efedffe027e533b1bcc0dbf270532e48285 /lisp/progmodes/flymake.el | |
parent | 4c49ec7f865bdad1629d2f125f71f4e506b258f2 (diff) | |
parent | 810fa21d26453f898de9747ece7205dfe6de9d08 (diff) | |
download | emacs-4dd1f56f29fc598a8339a345c2f8945250600602.tar.gz emacs-4dd1f56f29fc598a8339a345c2f8945250600602.tar.bz2 emacs-4dd1f56f29fc598a8339a345c2f8945250600602.zip |
Merge branch 'master' of git.savannah.gnu.org:/srv/git/emacs into feature/pgtk
Diffstat (limited to 'lisp/progmodes/flymake.el')
-rw-r--r-- | lisp/progmodes/flymake.el | 757 |
1 files changed, 540 insertions, 217 deletions
diff --git a/lisp/progmodes/flymake.el b/lisp/progmodes/flymake.el index e10602ab081..403925c8557 100644 --- a/lisp/progmodes/flymake.el +++ b/lisp/progmodes/flymake.el @@ -4,9 +4,9 @@ ;; Author: Pavel Kobyakov <pk_at_work@yahoo.com> ;; Maintainer: João Távora <joaotavora@gmail.com> -;; Version: 1.1.1 +;; Version: 1.2.1 ;; Keywords: c languages tools -;; Package-Requires: ((emacs "26.1") (eldoc "1.1.0")) +;; Package-Requires: ((emacs "26.1") (eldoc "1.1.0") (project "0.7.1")) ;; This is a GNU ELPA :core package. Avoid functionality that is not ;; compatible with the version of Emacs recorded above. @@ -121,6 +121,7 @@ (require 'mwheel) ;; when-let*, if-let*, hash-table-keys, hash-table-values: (eval-when-compile (require 'subr-x)) +(require 'project) (defgroup flymake nil "Universal on-the-fly syntax checker." @@ -265,7 +266,9 @@ If set to nil, don't suppress any zero counters." (warning-type-format (format " [%s %s]" (or sublog 'flymake) - (current-buffer)))) + ;; Handle file names with "%" correctly. (Bug#51549) + (string-replace "%" "%%" + (buffer-name (current-buffer)))))) (display-warning (list 'flymake sublog) (apply #'format-message msg args) (if (numberp level) @@ -291,7 +294,7 @@ generated it." (macroexp-file-name) (and (not load-file-name) (bound-and-true-p byte-compile-current-file)))) - (sublog (if file + (sublog (if (stringp file) (intern (file-name-nondirectory (file-name-sans-extension file)))))) @@ -305,35 +308,51 @@ generated it." (cl-defstruct (flymake--diag (:constructor flymake--diag-make)) - buffer beg end type text backend data overlay-properties overlay) + locus beg end type text backend data overlay-properties overlay + ;; FIXME: See usage of these two in `flymake--highlight-line'. + ;; Ideally they wouldn't be needed. + orig-beg orig-end) ;;;###autoload -(defun flymake-make-diagnostic (buffer +(defun flymake-make-diagnostic (locus beg end type text &optional data overlay-properties) - "Make a Flymake diagnostic for BUFFER's region from BEG to END. + "Make a Flymake diagnostic for LOCUS's region from BEG to END. +LOCUS is a buffer object or a string designating a file name. + TYPE is a diagnostic symbol and TEXT is string describing the problem detected in this region. DATA is any object that the caller wishes to attach to the created diagnostic for later -retrieval. +retrieval with `flymake-diagnostic-data'. + +If LOCUS is a buffer BEG and END should be buffer positions +inside it. If LOCUS designates a file, BEG and END should be a +cons (LINE . COL) indicating a file position. In this second +case, END may be ommited in which case the region is computed +using `flymake-diag-region' if the diagnostic is appended to an +actual buffer. OVERLAY-PROPERTIES is an alist of properties attached to the created diagnostic, overriding the default properties and any -properties of `flymake-overlay-control' of the diagnostic's -type." - (flymake--diag-make :buffer buffer :beg beg :end end +properties listed in the `flymake-overlay-control' property of +the diagnostic's type symbol." + (when (stringp locus) + (setq locus (expand-file-name locus))) + (flymake--diag-make :locus locus :beg beg :end end :type type :text text :data data - :overlay-properties overlay-properties)) + :overlay-properties overlay-properties + :orig-beg beg + :orig-end end)) ;;;###autoload (defun flymake-diagnostics (&optional beg end) "Get Flymake diagnostics in region determined by BEG and END. -If neither BEG or END is supplied, use the whole buffer, +If neither BEG or END is supplied, use whole accessible buffer, otherwise if BEG is non-nil and END is nil, consider only diagnostics at BEG." (mapcar (lambda (ov) (overlay-get ov 'flymake-diagnostic)) @@ -345,27 +364,13 @@ diagnostics at BEG." ,(format "Get Flymake diagnostic DIAG's %s." (symbol-name thing)) (,internal diag))) -(flymake--diag-accessor flymake-diagnostic-buffer flymake--diag-buffer buffer) (flymake--diag-accessor flymake-diagnostic-text flymake--diag-text text) (flymake--diag-accessor flymake-diagnostic-type flymake--diag-type type) (flymake--diag-accessor flymake-diagnostic-backend flymake--diag-backend backend) -(flymake--diag-accessor flymake-diagnostic-data flymake--diag-data backend) - -(defun flymake-diagnostic-beg (diag) - "Get Flymake diagnostic DIAG's start position. -This position only be queried after DIAG has been reported to Flymake." - (let ((overlay (flymake--diag-overlay diag))) - (unless overlay - (error "DIAG %s not reported to Flymake yet" diag)) - (overlay-start overlay))) - -(defun flymake-diagnostic-end (diag) - "Get Flymake diagnostic DIAG's end position. -This position only be queried after DIAG has been reported to Flymake." - (let ((overlay (flymake--diag-overlay diag))) - (unless overlay - (error "DIAG %s not reported to Flymake yet" diag)) - (overlay-end overlay))) +(flymake--diag-accessor flymake-diagnostic-data flymake--diag-data data) +(flymake--diag-accessor flymake-diagnostic-beg flymake--diag-beg beg) +(flymake--diag-accessor flymake-diagnostic-end flymake--diag-end end) +(flymake--diag-accessor flymake-diagnostic-buffer flymake--diag-locus locus) (cl-defun flymake--overlays (&key beg end filter compare key) "Get flymake-related overlays. @@ -556,7 +561,7 @@ Currently accepted REPORT-KEY arguments are: (put :warning 'flymake-category 'flymake-warning) (put :note 'flymake-category 'flymake-note) -(defvar flymake-diagnostic-types-alist '() "") +(defvar flymake-diagnostic-types-alist '()) (make-obsolete-variable 'flymake-diagnostic-types-alist "Set properties on the diagnostic symbols instead. See Info @@ -625,13 +630,74 @@ associated `flymake-category' return DEFAULT." bitmap (list bitmap))))))) -(defun flymake--highlight-line (diagnostic) - "Highlight buffer with info in DIGNOSTIC." - (let ((type (or (flymake--diag-type diagnostic) - :error)) - (ov (make-overlay - (flymake--diag-beg diagnostic) - (flymake--diag-end diagnostic)))) +(defun flymake--equal-diagnostic-p (a b) + "Tell if A and B are equivalent `flymake--diag' objects." + (or (eq a b) + (cl-loop for comp in '(flymake--diag-end + flymake--diag-beg + flymake-diagnostic-type + flymake-diagnostic-backend + flymake-diagnostic-text) + always (equal (funcall comp a) (funcall comp b))))) + +(cl-defun flymake--highlight-line (diagnostic &optional foreign) + "Attempt to overlay DIAGNOSTIC in current buffer. + +FOREIGN says if DIAGNOSTIC is \"foreign\" to the current buffer, +i.e. managed by another buffer where `flymake-mode' is also +active. + +This function mayskip overlay creation if a diagnostic which is +the same as DIAGNOSTIC is already highlighted +(in the sense of `flymake--equal-diagnostic-p'). In that case +the action to take depends on FOREIGN. If nil the existing +overlay is deleted, else no overlay is created. + +Return nil or the overlay created." + (let* ((type (or (flymake-diagnostic-type diagnostic) + :error)) + (beg (flymake--diag-beg diagnostic)) + (end (flymake--diag-end diagnostic)) + (convert (lambda (cell) + (flymake-diag-region (current-buffer) + (car cell) + (cdr cell)))) + ov) + ;; Convert (LINE . COL) forms of `flymake--diag-beg' and + ;; `flymake--diag-end'. Record the converted positions. + ;; + (cond ((and (consp beg) (not (null end))) + (setq beg (car (funcall convert beg))) + (when (consp end) + (setq end (car (funcall convert end))))) + ((consp beg) + (cl-destructuring-bind (a . b) (funcall convert beg) + (setq beg a end b)))) + (setf (flymake--diag-beg diagnostic) beg + (flymake--diag-end diagnostic) end) + ;; Try to fix the remedy the situation if there is the same + ;; diagnostic is already registered in the same place, which only + ;; happens for clashes between domestic and foreign diagnostics + (cl-loop for e in (flymake-diagnostics beg end) + when (flymake--equal-diagnostic-p e diagnostic) + ;; FIXME. This is an imperfect heuristic. Ideally, we'd + ;; want to delete no overlays and keep annotating the + ;; superseded foreign in an overlay but hide it from most + ;; `flymake-diagnostics' calls. If the target buffer is + ;; killed we can keep the "latent" state of the foreign + ;; diagnostic (with filename and updated line/col info). + ;; If it is revisited the foreign diagnostic can be + ;; revived again. + do (if foreign + (cl-return-from flymake--highlight-line nil) + (setf (flymake--diag-beg e) + (flymake--diag-orig-beg e) + (flymake--diag-end e) + (flymake--diag-orig-end e)) + (delete-overlay (flymake--diag-overlay e)))) + (setq ov (make-overlay end beg)) + (setf (flymake--diag-beg diagnostic) (overlay-start ov) + (flymake--diag-end diagnostic) (overlay-end ov)) ;; First set `category' in the overlay ;; (overlay-put ov 'category @@ -665,7 +731,7 @@ associated `flymake-category' return DEFAULT." (lambda (window _ov pos) (with-selected-window window (mapconcat - #'flymake--diag-text + #'flymake-diagnostic-text (flymake-diagnostics pos) "\n")))) (default-maybe 'severity (warning-numeric-level :error)) @@ -676,17 +742,18 @@ associated `flymake-category' return DEFAULT." ;; (overlay-put ov 'evaporate t) (overlay-put ov 'flymake-diagnostic diagnostic) + (setf (flymake--diag-overlay diagnostic) ov) ov)) ;; Nothing in Flymake uses this at all any more, so this is just for ;; third-party compatibility. (define-obsolete-function-alias 'flymake-display-warning 'message-box "26.1") -(defvar-local flymake--backend-state nil - "Buffer-local hash table of a Flymake backend's state. +(defvar-local flymake--state nil + "State of a buffer's multiple Flymake backends. The keys to this hash table are functions as found in `flymake-diagnostic-functions'. The values are structures -of the type `flymake--backend-state', with these slots: +of the type `flymake--state', with these slots: `running', a symbol to keep track of a backend's replies via its REPORT-FN argument. A backend is running if this key is @@ -701,11 +768,16 @@ since it last was contacted. `disabled', a string with the explanation for a previous exceptional situation reported by the backend, nil if the -backend is operating normally.") +backend is operating normally. + +`foreign-diags', a hash table of buffers/files to +collections of diagnostics outside the buffer where this +`flymake--state' pertains.") -(cl-defstruct (flymake--backend-state +(cl-defstruct (flymake--state (:constructor flymake--make-backend-state)) - running reported-p disabled diags) + running reported-p disabled diags (foreign-diags + (make-hash-table))) (defmacro flymake--with-backend-state (backend state-var &rest body) "Bind BACKEND's STATE-VAR to its state, run BODY." @@ -713,9 +785,9 @@ backend is operating normally.") (let ((b (make-symbol "b"))) `(let* ((,b ,backend) (,state-var - (or (gethash ,b flymake--backend-state) + (or (gethash ,b flymake--state) (puthash ,b (flymake--make-backend-state) - flymake--backend-state)))) + flymake--state)))) ,@body))) (defun flymake-is-running () @@ -730,9 +802,10 @@ backend is operating normally.") (and (>= start1 start0) (< start1 end0)) (and (> end1 start0) (<= end1 end0)))) -(cl-defun flymake--handle-report (backend token report-action - &key explanation force region - &allow-other-keys) +(cl-defun flymake--handle-report + (backend token report-action + &key explanation force region + &allow-other-keys) "Handle reports from BACKEND identified by TOKEN. BACKEND, REPORT-ACTION and EXPLANATION, and FORCE conform to the calling convention described in @@ -740,81 +813,117 @@ calling convention described in to handle a report even if TOKEN was not expected. REGION is a (BEG . END) pair of buffer positions indicating that this report applies to that region." - (let* ((state (gethash backend flymake--backend-state)) - first-report) - (unless state - (error "Can't find state for %s in `flymake--backend-state'" backend)) - (setf first-report (not (flymake--backend-state-reported-p state))) - (setf (flymake--backend-state-reported-p state) t) - (let (expected-token - new-diags) - (cond - ((null state) - (flymake-error - "Unexpected report from unknown backend %s" backend)) - ((flymake--backend-state-disabled state) - (flymake-error - "Unexpected report from disabled backend %s" backend)) - ((progn - (setq expected-token (flymake--backend-state-running state)) - (null expected-token)) - ;; should never happen - (flymake-error "Unexpected report from stopped backend %s" backend)) - ((not (or (eq expected-token token) - force)) - (flymake-error "Obsolete report from backend %s with explanation %s" - backend explanation)) - ((eq :panic report-action) - (flymake--disable-backend backend explanation)) - ((not (listp report-action)) - (flymake--disable-backend backend - (format "Unknown action %S" report-action)) - (flymake-error "Expected report, but got unknown key %s" report-action)) - (t - (setq new-diags - (cl-remove-if-not - (lambda (diag) (eq (flymake--diag-buffer diag) (current-buffer))) - report-action)) - (save-restriction - (widen) - ;; Before adding to backend's diagnostic list, decide if - ;; some or all must be deleted. When deleting, also delete - ;; the associated overlay. - (cond - (region - (cl-loop for diag in (flymake--backend-state-diags state) - for ov = (flymake--diag-overlay diag) - if (or (not (overlay-buffer ov)) - (flymake--intersects-p - (overlay-start ov) (overlay-end ov) - (car region) (cdr region))) - do (delete-overlay ov) - else collect diag into surviving - finally (setf (flymake--backend-state-diags state) - surviving))) - (first-report - (dolist (diag (flymake--backend-state-diags state)) - (delete-overlay (flymake--diag-overlay diag))) - (setf (flymake--backend-state-diags state) nil))) - ;; Now make new ones - (mapc (lambda (diag) - (let ((overlay (flymake--highlight-line diag))) - (setf (flymake--diag-backend diag) backend - (flymake--diag-overlay diag) overlay))) - new-diags) - (setf (flymake--backend-state-diags state) - (append new-diags (flymake--backend-state-diags state))) - (when flymake-check-start-time - (flymake-log :debug "backend %s reported %d diagnostics in %.2f second(s)" - backend - (length new-diags) - (float-time - (time-since flymake-check-start-time)))) - (when (and (get-buffer (flymake--diagnostics-buffer-name)) - (get-buffer-window (flymake--diagnostics-buffer-name)) - (null (cl-set-difference (flymake-running-backends) - (flymake-reporting-backends)))) - (flymake-show-diagnostics-buffer)))))))) + (let ((state (or (gethash backend flymake--state) + (error "Can't find state for %s in `flymake--state'" + backend))) + expected-token) + (cond + ((null state) + (flymake-error + "Unexpected report from unknown backend %s" backend)) + ((flymake--state-disabled state) + (flymake-error + "Unexpected report from disabled backend %s" backend)) + ((progn + (setq expected-token (flymake--state-running state)) + (null expected-token)) + ;; should never happen + (flymake-error "Unexpected report from stopped backend %s" backend)) + ((not (or (eq expected-token token) + force)) + (flymake-error "Obsolete report from backend %s with explanation %s" + backend explanation)) + ((eq :panic report-action) + (flymake--disable-backend backend explanation)) + ((not (listp report-action)) + (flymake--disable-backend backend + (format "Unknown action %S" report-action)) + (flymake-error "Expected report, but got unknown key %s" report-action)) + (t + (flymake--publish-diagnostics report-action + :backend backend + :state state + :region region) + (when flymake-check-start-time + (flymake-log :debug "backend %s reported %d diagnostics in %.2f second(s)" + backend + (length report-action) + (float-time + (time-since flymake-check-start-time)))))) + (setf (flymake--state-reported-p state) t) + (flymake--update-diagnostics-listings (current-buffer)))) + +(defun flymake--clear-foreign-diags (state) + (maphash (lambda (_buffer diags) + (cl-loop for d in diags + when (flymake--diag-overlay d) + do (delete-overlay it))) + (flymake--state-foreign-diags state)) + (clrhash (flymake--state-foreign-diags state))) + +(defvar-local flymake-mode nil) + +(cl-defun flymake--publish-diagnostics (diags &key backend state region) + "Helper for `flymake--handle-report'. +Publish DIAGS, which contain diagnostics for the current buffer +and other buffers." + (dolist (d diags) (setf (flymake--diag-backend d) backend)) + (save-restriction + (widen) + ;; First, clean up. Remove diagnostics from bookeeping lists and + ;; their overlays from buffers. + ;; + (cond + (;; If there is a `region' arg, only affect the diagnostics whose + ;; overlays are in a certain region. Discard "foreign" + ;; diagnostics. + region + (cl-loop for diag in (flymake--state-diags state) + for ov = (flymake--diag-overlay diag) + if (or (not (overlay-buffer ov)) + (flymake--intersects-p + (overlay-start ov) (overlay-end ov) + (car region) (cdr region))) + do (delete-overlay ov) + else collect diag into surviving + finally (setf (flymake--state-diags state) + surviving))) + (;; Else, if this is the first report, zero all lists and delete + ;; all associated overlays. + (not (flymake--state-reported-p state)) + (cl-loop for diag in (flymake--state-diags state) + for ov = (flymake--diag-overlay diag) + when ov do (delete-overlay ov)) + (setf (flymake--state-diags state) nil) + ;; Also clear all overlays for `foreign-diags' in all other + ;; buffers. + (flymake--clear-foreign-diags state)) + (;; If this is not the first report, do no cleanup. + t)) + + ;; Now place new overlays for all diagnostics: "domestic" + ;; diagnostics are for the current buffer; "foreign" may be for a + ;; some other live buffer or for a file name that hasn't a buffer + ;; yet. If a foreign diagnostic is for a buffer, convert to a + ;; file name, protecting it against that buffer's killing. + ;; + (cl-loop + for d in diags + for locus = (flymake--diag-locus d) + do (cond ((eq locus (current-buffer)) + (push d (flymake--state-diags state)) + (flymake--highlight-line d)) + (t + (when (or (buffer-live-p locus) + (setq locus (find-buffer-visiting locus))) + (with-current-buffer locus + (when flymake-mode (flymake--highlight-line d 'foreign)) + ;; Ensure locus of a foreign diag is always a file-name + ;; string, even if created from a buffer. + (setf (flymake--diag-locus d) (buffer-file-name)))) + (cl-assert (stringp (flymake--diag-locus d))) + (push d (gethash (flymake--diag-locus d) + (flymake--state-foreign-diags state)))))))) (defun flymake-make-report-fn (backend &optional token) "Make a suitable anonymous report function for BACKEND. @@ -830,12 +939,12 @@ different runs of the same backend." (defun flymake--collect (fn &optional message-prefix) "Collect Flymake backends matching FN. If MESSAGE-PREFIX, echo a message using that prefix." - (unless flymake--backend-state + (unless flymake--state (user-error "Flymake is not initialized")) (let (retval) (maphash (lambda (backend state) (when (funcall fn state) (push backend retval))) - flymake--backend-state) + flymake--state) (when message-prefix (message "%s%s" message-prefix @@ -846,21 +955,21 @@ If MESSAGE-PREFIX, echo a message using that prefix." (defun flymake-running-backends () "Compute running Flymake backends in current buffer." (interactive) - (flymake--collect #'flymake--backend-state-running + (flymake--collect #'flymake--state-running (and (called-interactively-p 'interactive) "Running backends: "))) (defun flymake-disabled-backends () "Compute disabled Flymake backends in current buffer." (interactive) - (flymake--collect #'flymake--backend-state-disabled + (flymake--collect #'flymake--state-disabled (and (called-interactively-p 'interactive) "Disabled backends: "))) (defun flymake-reporting-backends () "Compute reporting Flymake backends in current buffer." (interactive) - (flymake--collect #'flymake--backend-state-reported-p + (flymake--collect #'flymake--state-reported-p (and (called-interactively-p 'interactive) "Reporting backends: "))) @@ -869,9 +978,9 @@ If MESSAGE-PREFIX, echo a message using that prefix." If it is running also stop it." (flymake-log :warning "Disabling backend %s because %s" backend explanation) (flymake--with-backend-state backend state - (setf (flymake--backend-state-running state) nil - (flymake--backend-state-disabled state) explanation - (flymake--backend-state-reported-p state) t))) + (setf (flymake--state-running state) nil + (flymake--state-disabled state) explanation + (flymake--state-reported-p state) t))) (defun flymake--run-backend (backend &optional args) "Run the backend BACKEND, re-enabling if necessary. @@ -880,9 +989,9 @@ with a report function." (flymake-log :debug "Running backend %s" backend) (let ((run-token (cl-gensym "backend-token"))) (flymake--with-backend-state backend state - (setf (flymake--backend-state-running state) run-token - (flymake--backend-state-disabled state) nil - (flymake--backend-state-reported-p state) nil)) + (setf (flymake--state-running state) run-token + (flymake--state-disabled state) nil + (flymake--state-reported-p state) nil)) ;; FIXME: Should use `condition-case-unless-debug' here, but don't ;; for two reasons: (1) that won't let me catch errors from inside ;; `ert-deftest' where `debug-on-error' appears to be always @@ -964,7 +1073,7 @@ Interactively, with a prefix arg, FORCE is t." (cond ((and (not force) (flymake--with-backend-state backend state - (flymake--backend-state-disabled state))) + (flymake--state-disabled state))) (flymake-log :debug "Backend %s is disabled, not starting" backend)) (t @@ -973,7 +1082,7 @@ Interactively, with a prefix arg, FORCE is t." (defvar flymake-mode-map (let ((map (make-sparse-keymap))) map) - "Keymap for `flymake-mode'") + "Keymap for `flymake-mode'.") ;;;###autoload (define-minor-mode flymake-mode @@ -1019,13 +1128,43 @@ special *Flymake log* buffer." :group 'flymake :lighter ;; If Flymake happened to be already already ON, we must cleanup ;; existing diagnostic overlays, lest we forget them by blindly - ;; reinitializing `flymake--backend-state' in the next line. + ;; reinitializing `flymake--state' in the next line. ;; See https://github.com/joaotavora/eglot/issues/223. (mapc #'delete-overlay (flymake--overlays)) - (setq flymake--backend-state (make-hash-table)) + (setq flymake--state (make-hash-table)) (setq flymake--recent-changes nil) - (when flymake-start-on-flymake-mode (flymake-start t))) + (when flymake-start-on-flymake-mode (flymake-start t)) + + ;; Other diagnostic sources may already target this buffer's file + ;; before we turned on: these sources may be of two types... + (let ((source (current-buffer)) + (bfn buffer-file-name)) + ;; 1. For `flymake-list-only-diagnostics': here, we do nothing. + ;; FIXME: We could remove the corresponding entry from that + ;; variable, as we assume that new diagnostics will come in soon + ;; via the brand new `flymake-mode' setup. For simplicity's + ;; sake, we have opted to leave the backend for now. + nil + ;; 2. other buffers where a backend has created "foreign" + ;; diagnostics and pointed them here. We must highlight them in + ;; this buffer, i.e. create overlays for them. Those other + ;; buffers and backends are still responsible for them, i.e. the + ;; current buffer does not "own" these foreign diags. + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (when (and flymake-mode flymake--state) + (maphash (lambda (_backend state) + (maphash (lambda (file diags) + (when (or (eq file source) + (string= bfn (expand-file-name file))) + (with-current-buffer source + (mapc (lambda (diag) + (flymake--highlight-line diag + 'foreign)) + diags)))) + (flymake--state-foreign-diags state))) + flymake--state)))))) ;; Turning the mode OFF. (t @@ -1035,11 +1174,16 @@ special *Flymake log* buffer." :group 'flymake :lighter ;;+(remove-hook 'find-file-hook (function flymake-find-file-hook) t) (remove-hook 'eldoc-documentation-functions 'flymake-eldoc-function t) - (mapc #'delete-overlay (flymake--overlays)) - (when flymake-timer (cancel-timer flymake-timer) - (setq flymake-timer nil))))) + (setq flymake-timer nil)) + (mapc #'delete-overlay (flymake--overlays)) + (when flymake--state + (maphash (lambda (_backend state) + (flymake--clear-foreign-diags state)) + flymake--state))) + ;; turning Flymake on or off has consequences for listings + (flymake--update-diagnostics-listings (current-buffer)))) (defun flymake--schedule-timer-maybe () "(Re)schedule an idle timer for checking the buffer. @@ -1091,9 +1235,9 @@ START and STOP and LEN are as in `after-change-functions'." (flymake-start t))) (defun flymake-kill-buffer-hook () - (when flymake-timer - (cancel-timer flymake-timer) - (setq flymake-timer nil))) + ;; Explicitly set flymake off, because that does a lot of useful + ;; cleanup. + (flymake-mode -1)) (defun flymake-find-file-hook () (unless (or flymake-mode @@ -1137,7 +1281,7 @@ default) no filter is applied." (not filter) (cl-find (flymake--severity - (flymake--diag-type diag)) + (flymake-diagnostic-type diag)) filter :key #'flymake--severity))))) :compare (if (cl-plusp n) #'< #'>) :key #'overlay-start)) @@ -1187,12 +1331,12 @@ default) no filter is applied." ;;; Mode-line and menu ;;; -(easy-menu-define flymake-menu flymake-mode-map "Flymake" +(easy-menu-define flymake-menu flymake-mode-map "Flymake menu." '("Flymake" [ "Go to next problem" flymake-goto-next-error t ] [ "Go to previous problem" flymake-goto-prev-error t ] [ "Check now" flymake-start t ] - [ "List all problems" flymake-show-diagnostics-buffer t ] + [ "List all problems" flymake-show-buffer-diagnostics t ] "--" [ "Go to log buffer" flymake-switch-to-log-buffer t ] [ "Turn off Flymake" flymake-mode t ])) @@ -1245,17 +1389,19 @@ correctly.") "Flymake" mouse-face mode-line-highlight help-echo - (lambda (&rest whatever) - (concat - (format "%s known backends\n" (hash-table-count flymake--backend-state)) - (format "%s running\n" (length (flymake-running-backends))) - (format "%s disabled\n" (length (flymake-disabled-backends))) - "mouse-1: Display minor mode menu\n" - "mouse-2: Show help for minor mode")) + ,(lambda (&rest _) + (concat + (format "%s known backends\n" (hash-table-count flymake--state)) + (format "%s running\n" (length (flymake-running-backends))) + (format "%s disabled\n" (length (flymake-disabled-backends))) + "mouse-1: Display minor mode menu\n" + "mouse-2: Show help for minor mode")) keymap ,(let ((map (make-sparse-keymap))) (define-key map [mode-line down-mouse-1] flymake-menu) + (define-key map [mode-line down-mouse-3] + flymake-menu) (define-key map [mode-line mouse-2] (lambda () (interactive) @@ -1266,7 +1412,7 @@ correctly.") "Helper for `flymake-mode-line-exception'." (pcase-let* ((running) (reported) (`(,ind ,face ,explain) - (cond ((zerop (hash-table-count flymake--backend-state)) + (cond ((zerop (hash-table-count flymake--state)) '("?" nil "No known backends")) ((cl-set-difference (setq running (flymake-running-backends)) @@ -1298,13 +1444,10 @@ TYPE is usually keyword `:error', `:warning' or `:note'." (face (flymake--lookup-type-property type 'mode-line-face 'compilation-error))) - (maphash (lambda - (_b state) - (dolist (d (flymake--backend-state-diags state)) - (when (= (flymake--severity type) - (flymake--severity (flymake--diag-type d))) - (cl-incf count)))) - flymake--backend-state) + (dolist (d (flymake-diagnostics)) + (when (= (flymake--severity type) + (flymake--severity (flymake-diagnostic-type d))) + (cl-incf count))) (when (or (cl-plusp count) (cond ((eq flymake-suppress-zero-counters t) nil) @@ -1334,7 +1477,7 @@ TYPE is usually keyword `:error', `:warning' or `:note'." (flymake-goto-next-error 1 (list type) t)))) map)))))) -;;; Diagnostics buffer +;;; Per-buffer diagnostic listing (defvar-local flymake--diagnostics-buffer-source nil) @@ -1349,14 +1492,30 @@ TYPE is usually keyword `:error', `:warning' or `:note'." (interactive (list (point) t)) (let* ((id (or (tabulated-list-get-id pos) (user-error "Nothing at point"))) - (diag (plist-get id :diagnostic))) - (with-current-buffer (flymake--diag-buffer diag) + (diag (plist-get id :diagnostic)) + (locus (flymake--diag-locus diag)) + (beg (flymake--diag-beg diag)) + (end (flymake--diag-end diag)) + (visit (lambda (b e) + (goto-char b) + (pulse-momentary-highlight-region (point) + (or e (line-end-position)) + 'highlight)))) + (with-current-buffer (cond ((bufferp locus) locus) + (t (find-file-noselect locus))) (with-selected-window (display-buffer (current-buffer) other-window) - (goto-char (flymake--diag-beg diag)) - (pulse-momentary-highlight-region (flymake-diagnostic-beg diag) - (flymake-diagnostic-end diag) - 'highlight)) + (cond (;; an annotated diagnostic (most common case), or a + ;; non-annotated buffer diag + (number-or-marker-p beg) + (funcall visit beg end)) + (;; a non-annotated file diag (TODO: could use `end' + ;; here, too) + (pcase-let ((`(,bbeg . ,bend) + (flymake-diag-region (current-buffer) + (car beg) + (cdr beg)))) + (funcall visit bbeg bend))))) (current-buffer)))) (defun flymake-goto-diagnostic (pos) @@ -1366,65 +1525,116 @@ POS can be a buffer position or a button" (pop-to-buffer (flymake-show-diagnostic (if (button-type pos) (button-start pos) pos)))) +(defun flymake--tabulated-entries-1 (diags project-root) + "Helper for `flymake--diagnostic-buffer-entries'. +PROJECT-ROOT indicates that each entry should be preceded by the +filename of the diagnostic relative to that directory." + (cl-loop + for diag in diags + for locus = (flymake-diagnostic-buffer diag) + for file = (if (bufferp locus) + (buffer-file-name locus) + locus) + for overlay = (flymake--diag-overlay diag) + for (line . col) = + (cond (;; has live overlay, use overlay for position + (and overlay (overlay-buffer overlay)) + (with-current-buffer (overlay-buffer overlay) + (save-excursion + (goto-char (overlay-start overlay)) + (cons (line-number-at-pos) + (- (point) + (line-beginning-position)))))) + (;; diagnostic not annotated, maybe foreign, check for cons + (consp (flymake--diag-beg diag)) + (flymake--diag-beg diag)) + (;; may still be a valid foreign diagnostic + (consp (flymake--diag-orig-beg diag)) + (flymake--diag-orig-beg diag)) + (;; somehow dead annotated diagnostic, ignore/give up + t nil)) + for type = (flymake-diagnostic-type diag) + for backend = (flymake-diagnostic-backend diag) + for bname = (or (ignore-errors (symbol-name backend)) + "(anonymous function)") + for data-vec = `[,(format "%s" line) + ,(format "%s" col) + ,(propertize (format "%s" + (flymake--lookup-type-property + type 'flymake-type-name type)) + 'face (flymake--lookup-type-property + type 'mode-line-face 'flymake-error)) + ,(propertize + (if bname + (replace-regexp-in-string "\\(.\\)[^-]+\\(-\\|$\\)" + "\\1\\2" bname) + "(anon)") + 'help-echo (format "From `%s' backend" backend)) + (,(replace-regexp-in-string "\n.*" "" + (flymake-diagnostic-text diag)) + mouse-face highlight + help-echo "mouse-2: visit this diagnostic" + face nil + action flymake-goto-diagnostic + mouse-action flymake-goto-diagnostic)] + when (and line col) collect + (list (list :diagnostic diag + :line line + :severity (flymake--lookup-type-property + type + 'severity (warning-numeric-level :error))) + (if project-root + (vconcat `[(,(file-name-nondirectory file) + help-echo ,(file-relative-name file project-root) + face nil + mouse-face highlight + action flymake-goto-diagnostic + mouse-action flymake-goto-diagnostic )] + data-vec) + data-vec)))) + (defun flymake--diagnostics-buffer-entries () + "Get tabulated list entries for current tabulated list buffer. +Expects `flymake--diagnostics-buffer-entries' to be bound to a +buffer." ;; Do nothing if 'flymake--diagnostics-buffer-source' has not yet ;; been set to a valid buffer. This could happen when this function ;; is called too early. For example 'global-display-line-numbers-mode' ;; calls us from its mode hook, when the diagnostic buffer has just - ;; been created by 'flymake-show-diagnostics-buffer', but is not yet - ;; set up properly. + ;; been created by 'flymake-show-buffer-diagnostics', but is not yet + ;; set up properly (Bug#40529). (when (bufferp flymake--diagnostics-buffer-source) (with-current-buffer flymake--diagnostics-buffer-source - (cl-loop for diag in - (cl-sort (flymake-diagnostics) #'< :key #'flymake-diagnostic-beg) - for (line . col) = - (save-excursion - (goto-char (flymake--diag-beg diag)) - (cons (line-number-at-pos) - (- (point) - (line-beginning-position)))) - for type = (flymake--diag-type diag) - collect - (list (list :diagnostic diag - :line line - :severity (flymake--lookup-type-property - type - 'severity (warning-numeric-level :error))) - `[,(format "%s" line) - ,(format "%s" col) - ,(propertize (format "%s" - (flymake--lookup-type-property - type 'flymake-type-name type)) - 'face (flymake--lookup-type-property - type 'mode-line-face 'flymake-error)) - (,(format "%s" (flymake--diag-text diag)) - mouse-face highlight - help-echo "mouse-2: visit this diagnostic" - face nil - action flymake-goto-diagnostic - mouse-action flymake-goto-diagnostic)]))))) + (when flymake-mode + (flymake--tabulated-entries-1 (flymake-diagnostics) nil))))) + +(defvar flymake--diagnostics-base-tabulated-list-format + `[("Line" 5 ,(lambda (l1 l2) + (< (plist-get (car l1) :line) + (plist-get (car l2) :line))) + :right-align t) + ("Col" 3 nil :right-align t) + ("Type" 8 ,(lambda (l1 l2) + (< (plist-get (car l1) :severity) + (plist-get (car l2) :severity)))) + ("Backend" 8 t) + ("Message" 0 t)]) (define-derived-mode flymake-diagnostics-buffer-mode tabulated-list-mode "Flymake diagnostics" "A mode for listing Flymake diagnostics." - (setq tabulated-list-format - `[("Line" 5 ,(lambda (l1 l2) - (< (plist-get (car l1) :line) - (plist-get (car l2) :line))) - :right-align t) - ("Col" 3 nil :right-align t) - ("Type" 8 ,(lambda (l1 l2) - (< (plist-get (car l1) :severity) - (plist-get (car l2) :severity)))) - ("Message" 0 t)]) + (setq tabulated-list-format flymake--diagnostics-base-tabulated-list-format) (setq tabulated-list-entries 'flymake--diagnostics-buffer-entries) (tabulated-list-init-header)) (defun flymake--diagnostics-buffer-name () - (format "*Flymake diagnostics for %s*" (current-buffer))) + (format "*Flymake diagnostics for `%s'*" (current-buffer))) -(defun flymake-show-diagnostics-buffer () +(define-obsolete-function-alias 'flymake-show-diagnostics-buffer + 'flymake-show-buffer-diagnostics "1.2.1") + +(defun flymake-show-buffer-diagnostics () "Show a list of Flymake diagnostics for current buffer." (interactive) (let* ((name (flymake--diagnostics-buffer-name)) @@ -1435,8 +1645,121 @@ POS can be a buffer position or a button" (current-buffer))))) (with-current-buffer target (setq flymake--diagnostics-buffer-source source) - (revert-buffer) - (display-buffer (current-buffer))))) + (display-buffer (current-buffer)) + (revert-buffer)))) + + +;;; Per-project diagnostic listing +;;; + +(defvar flymake-list-only-diagnostics nil + "Diagnostics list meant for listing, not highlighting. +This variable holds an alist ((FILE-NAME . DIAGS) ...) where +FILE-NAME is a string holding an absolute file name and DIAGS is +a list of diagnostic objects created with +`flymake-make-diagnostic'. These diagnostics are never annotated +as overlays in actual buffers: they merely serve as temporary +stand-ins for more accurate diagnostics that are produced once +the file they refer to is visited and `flymake-mode' is turned on +in the resulting buffer. + +Flymake backends that somehow gain sporadic information about +diagnostics in neighbouring files may freely modify this variable +by adding or removing entries to for those files. If the +information about those neighbouring files is acquired repeatedly +and reliably, it may be more sensible to report them as +\"foreign\" diagnostics instead. + +Commands such as `flymake-show-project-diagnostics' will include +some of this variable's contents the diagnostic listings.") + +(defvar-local flymake--project-diagnostic-list-project nil) + +(define-derived-mode flymake-project-diagnostics-mode tabulated-list-mode + "Flymake diagnostics" + "A mode for listing Flymake diagnostics." + (setq tabulated-list-format + (vconcat [("File" 25 t)] + flymake--diagnostics-base-tabulated-list-format)) + (setq tabulated-list-entries + 'flymake--project-diagnostics-entries) + (tabulated-list-init-header)) + +(cl-defun flymake--project-diagnostics (&optional (project (project-current))) + "Get all known relevant diagnostics for PROJECT." + (let* ((root (project-root project)) + (visited-buffers (cl-remove-if-not #'buffer-file-name (project-buffers project))) + buffer-annotated-diags + relevant-foreign-diags + list-only-diags + annotated-diag-files) + (setq buffer-annotated-diags + (cl-loop for buf in visited-buffers + for diags = (with-current-buffer buf + (flymake-diagnostics)) + when diags do + (push (buffer-file-name buf) annotated-diag-files) + append (cl-sort diags #'< :key #'flymake-diagnostic-beg))) + (cl-loop + for buf in visited-buffers + do (with-current-buffer buf + (when (and flymake-mode flymake--state) + (maphash + (lambda (_backend state) + (maphash + (lambda (foreign-file diags) + (setq foreign-file (expand-file-name foreign-file)) + ;; FIXME: This is not right if more than one visited + ;; source targets the same foreign file. Don't + ;; think we can get away without some kind of + ;; `cl-remove-duplicates' here that utilizes + ;; `flymake--equal-diagnostic-p'. + (unless (member foreign-file annotated-diag-files) + (push foreign-file annotated-diag-files) + (setq relevant-foreign-diags + (append relevant-foreign-diags + diags)))) + (flymake--state-foreign-diags state))) + flymake--state)))) + (setq list-only-diags + (cl-loop for (file-name . diags) in flymake-list-only-diagnostics + if (and (string-prefix-p (expand-file-name root) file-name) + (not (member file-name annotated-diag-files))) + append diags)) + (append buffer-annotated-diags relevant-foreign-diags list-only-diags))) + +(defun flymake--project-diagnostics-entries () + (let ((p (project-current))) + (flymake--tabulated-entries-1 (flymake--project-diagnostics p) + (project-root p)))) + +(defun flymake--project-diagnostics-buffer (root) + (get-buffer-create (format "*Flymake diagnostics for `%s'*" root))) + +(defun flymake-show-project-diagnostics () + "Show a list of Flymake diagnostics for the current project." + (interactive) + (let* ((prj (project-current)) + (root (project-root prj)) + (buffer (flymake--project-diagnostics-buffer root))) + (with-current-buffer buffer + (flymake-project-diagnostics-mode) + (setq-local flymake--project-diagnostic-list-project prj) + (display-buffer (current-buffer)) + (revert-buffer)))) + +(defun flymake--update-diagnostics-listings (buffer) + "Update diagnostics listings somehow relevant to BUFFER." + (dolist (probe (buffer-list)) + (with-current-buffer probe + (when (or (and (eq major-mode 'flymake-project-diagnostics-mode) + flymake--project-diagnostic-list-project + (buffer-file-name buffer) + (memq buffer + (project-buffers flymake--project-diagnostic-list-project))) + (and (eq major-mode 'flymake-diagnostics-buffer-mode) + (eq flymake--diagnostics-buffer-source buffer))) + (revert-buffer))))) (provide 'flymake) |