flymake.el (74201B)
1 ;;; flymake.el --- A universal on-the-fly syntax checker -*- lexical-binding: t; -*- 2 3 ;; Copyright (C) 2003-2021 Free Software Foundation, Inc. 4 5 ;; Author: Pavel Kobyakov <pk_at_work@yahoo.com> 6 ;; Maintainer: João Távora <joaotavora@gmail.com> 7 ;; Version: 1.2.1 8 ;; Keywords: c languages tools 9 ;; Package-Requires: ((emacs "26.1") (eldoc "1.1.0") (project "0.7.1")) 10 11 ;; This is a GNU ELPA :core package. Avoid functionality that is not 12 ;; compatible with the version of Emacs recorded above. 13 14 ;; This file is part of GNU Emacs. 15 16 ;; GNU Emacs is free software: you can redistribute it and/or modify 17 ;; it under the terms of the GNU General Public License as published by 18 ;; the Free Software Foundation, either version 3 of the License, or 19 ;; (at your option) any later version. 20 21 ;; GNU Emacs is distributed in the hope that it will be useful, but 22 ;; WITHOUT ANY WARRANTY; without even the implied warranty of 23 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 24 ;; General Public License for more details. 25 26 ;; You should have received a copy of the GNU General Public License 27 ;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. 28 29 ;;; Commentary: 30 ;; 31 ;; Flymake is a minor Emacs mode performing on-the-fly syntax checks. 32 ;; 33 ;; Flymake collects diagnostic information for multiple sources, 34 ;; called backends, and visually annotates the relevant portions in 35 ;; the buffer. 36 ;; 37 ;; This file contains the UI for displaying and interacting with the 38 ;; results produced by these backends, as well as entry points for 39 ;; backends to hook on to. 40 ;; 41 ;; The main interactive entry point is the `flymake-mode' minor mode, 42 ;; which periodically and automatically initiates checks as the user 43 ;; is editing the buffer. The variables `flymake-no-changes-timeout', 44 ;; `flymake-start-on-flymake-mode' give finer control over the events 45 ;; triggering a check, as does the interactive command `flymake-start', 46 ;; which immediately starts a check. 47 ;; 48 ;; Shortly after each check, a summary of collected diagnostics should 49 ;; appear in the mode-line. If it doesn't, there might not be a 50 ;; suitable Flymake backend for the current buffer's major mode, in 51 ;; which case Flymake will indicate this in the mode-line. The 52 ;; indicator will be `!' (exclamation mark), if all the configured 53 ;; backends errored (or decided to disable themselves) and `?' 54 ;; (question mark) if no backends were even configured. 55 ;; 56 ;; For programmers interested in writing a new Flymake backend, the 57 ;; docstring of `flymake-diagnostic-functions', the Flymake manual, 58 ;; and the code of existing backends are probably a good starting 59 ;; point. 60 ;; 61 ;; The user wishing to customize the appearance of error types should 62 ;; set properties on the symbols associated with each diagnostic type. 63 ;; The standard diagnostic symbols are `:error', `:warning' and 64 ;; `:note' (though a specific backend may define and use more). The 65 ;; following properties can be set: 66 ;; 67 ;; * `flymake-bitmap', an image displayed in the fringe according to 68 ;; `flymake-fringe-indicator-position'. The value actually follows 69 ;; the syntax of `flymake-error-bitmap' (which see). It is overridden 70 ;; by any `before-string' overlay property. 71 ;; 72 ;; * `flymake-severity', a non-negative integer specifying the 73 ;; diagnostic's severity. The higher, the more serious. If the 74 ;; overlay property `priority' is not specified, `severity' is used to 75 ;; set it and help sort overlapping overlays. 76 ;; 77 ;; * `flymake-overlay-control', an alist ((OVPROP . VALUE) ...) of 78 ;; further properties used to affect the appearance of Flymake 79 ;; annotations. With the exception of `category' and `evaporate', 80 ;; these properties are applied directly to the created overlay. See 81 ;; Info Node `(elisp)Overlay Properties'. 82 ;; 83 ;; * `flymake-category', a symbol whose property list is considered a 84 ;; default for missing values of any other properties. This is useful 85 ;; to backend authors when creating new diagnostic types that differ 86 ;; from an existing type by only a few properties. The category 87 ;; symbols `flymake-error', `flymake-warning' and `flymake-note' make 88 ;; good candidates for values of this property. 89 ;; 90 ;; For instance, to omit the fringe bitmap displayed for the standard 91 ;; `:note' type, set its `flymake-bitmap' property to nil: 92 ;; 93 ;; (put :note 'flymake-bitmap nil) 94 ;; 95 ;; To change the face for `:note' type, add a `face' entry to its 96 ;; `flymake-overlay-control' property. 97 ;; 98 ;; (push '(face . highlight) (get :note 'flymake-overlay-control)) 99 ;; 100 ;; If you push another alist entry in front, it overrides the previous 101 ;; one. So this effectively removes the face from `:note' 102 ;; diagnostics. 103 ;; 104 ;; (push '(face . nil) (get :note 'flymake-overlay-control)) 105 ;; 106 ;; To erase customizations and go back to the original look for 107 ;; `:note' types: 108 ;; 109 ;; (cl-remf (symbol-plist :note) 'flymake-overlay-control) 110 ;; (cl-remf (symbol-plist :note) 'flymake-bitmap) 111 ;; 112 ;;; Code: 113 114 (require 'cl-lib) 115 (require 'thingatpt) ; end-of-thing 116 (require 'warnings) ; warning-numeric-level, display-warning 117 (require 'compile) ; for some faces 118 ;; We need the next require to avoid compiler warnings and run-time 119 ;; errors about mouse-wheel-up/down-event in builds --without-x, where 120 ;; mwheel is not preloaded. 121 (require 'mwheel) 122 ;; when-let*, if-let*, hash-table-keys, hash-table-values: 123 (eval-when-compile (require 'subr-x)) 124 (require 'project) 125 126 (defgroup flymake nil 127 "Universal on-the-fly syntax checker." 128 :version "23.1" 129 :link '(custom-manual "(flymake) Top") 130 :group 'tools) 131 132 (defcustom flymake-error-bitmap '(flymake-double-exclamation-mark 133 compilation-error) 134 "Bitmap (a symbol) used in the fringe for indicating errors. 135 The value may also be a list of two elements where the second 136 element specifies the face for the bitmap. For possible bitmap 137 symbols, see `fringe-bitmaps'. See also `flymake-warning-bitmap'. 138 139 The option `flymake-fringe-indicator-position' controls how and where 140 this is used." 141 :version "24.3" 142 :type '(choice (symbol :tag "Bitmap") 143 (list :tag "Bitmap and face" 144 (symbol :tag "Bitmap") 145 (face :tag "Face")))) 146 147 (defcustom flymake-warning-bitmap '(exclamation-mark compilation-warning) 148 "Bitmap (a symbol) used in the fringe for indicating warnings. 149 The value may also be a list of two elements where the second 150 element specifies the face for the bitmap. For possible bitmap 151 symbols, see `fringe-bitmaps'. See also `flymake-error-bitmap'. 152 153 The option `flymake-fringe-indicator-position' controls how and where 154 this is used." 155 :version "24.3" 156 :type '(choice (symbol :tag "Bitmap") 157 (list :tag "Bitmap and face" 158 (symbol :tag "Bitmap") 159 (face :tag "Face")))) 160 161 (defcustom flymake-note-bitmap '(exclamation-mark compilation-info) 162 "Bitmap (a symbol) used in the fringe for indicating info notes. 163 The value may also be a list of two elements where the second 164 element specifies the face for the bitmap. For possible bitmap 165 symbols, see `fringe-bitmaps'. See also `flymake-error-bitmap'. 166 167 The option `flymake-fringe-indicator-position' controls how and where 168 this is used." 169 :version "26.1" 170 :type '(choice (symbol :tag "Bitmap") 171 (list :tag "Bitmap and face" 172 (symbol :tag "Bitmap") 173 (face :tag "Face")))) 174 175 (defcustom flymake-fringe-indicator-position 'left-fringe 176 "The position to put Flymake fringe indicator. 177 The value can be nil (do not use indicators), `left-fringe' or `right-fringe'. 178 See `flymake-error-bitmap' and `flymake-warning-bitmap'." 179 :version "24.3" 180 :type '(choice (const left-fringe) 181 (const right-fringe) 182 (const :tag "No fringe indicators" nil))) 183 184 (make-obsolete-variable 'flymake-start-syntax-check-on-newline 185 "can check on newline in post-self-insert-hook" 186 "27.1") 187 188 (defcustom flymake-no-changes-timeout 0.5 189 "Time to wait after last change before automatically checking buffer. 190 If nil, never start checking buffer automatically like this." 191 :type '(choice (number :tag "Timeout in seconds") 192 (const :tag "No check on timeout" nil))) 193 194 (defcustom flymake-gui-warnings-enabled t 195 "Enables/disables GUI warnings." 196 :type 'boolean) 197 (make-obsolete-variable 'flymake-gui-warnings-enabled 198 "it no longer has any effect." "26.1") 199 200 (define-obsolete-variable-alias 'flymake-start-syntax-check-on-find-file 201 'flymake-start-on-flymake-mode "26.1") 202 203 (defcustom flymake-start-on-flymake-mode t 204 "If non-nil, start syntax check when `flymake-mode' is enabled. 205 Specifically, start it when the buffer is actually displayed." 206 :version "26.1" 207 :type 'boolean) 208 209 (defcustom flymake-start-on-save-buffer t 210 "If non-nil, start syntax check when a buffer is saved. 211 Specifically, start it when the saved buffer is actually displayed." 212 :version "27.1" 213 :type 'boolean) 214 215 (defcustom flymake-log-level -1 216 "Obsolete and ignored variable." 217 :type 'integer) 218 (make-obsolete-variable 'flymake-log-level 219 "it is superseded by `warning-minimum-log-level.'" 220 "26.1") 221 222 (defcustom flymake-wrap-around t 223 "If non-nil, moving to errors wraps around buffer boundaries." 224 :version "26.1" 225 :type 'boolean) 226 227 (defcustom flymake-suppress-zero-counters :warning 228 "Control appearance of zero-valued diagnostic counters in mode line. 229 230 If set to t, suppress all zero counters. If set to a severity 231 symbol like `:warning' (the default) suppress zero counters less 232 severe than that severity, according to `warning-numeric-level'. 233 If set to nil, don't suppress any zero counters." 234 :type 'symbol) 235 236 (when (fboundp 'define-fringe-bitmap) 237 (define-fringe-bitmap 'flymake-double-exclamation-mark 238 (vector #b00000000 239 #b00000000 240 #b00000000 241 #b00000000 242 #b01100110 243 #b01100110 244 #b01100110 245 #b01100110 246 #b01100110 247 #b01100110 248 #b01100110 249 #b01100110 250 #b00000000 251 #b01100110 252 #b00000000 253 #b00000000 254 #b00000000))) 255 256 (defvar-local flymake-timer nil 257 "Timer for starting syntax check.") 258 259 (defvar-local flymake-check-start-time nil 260 "Time at which syntax check was started.") 261 262 (defun flymake--log-1 (level sublog msg &rest args) 263 "Do actual work for `flymake-log'." 264 (let (;; never popup the log buffer 265 (warning-minimum-level :emergency) 266 (warning-type-format 267 (format " [%s %s]" 268 (or sublog 'flymake) 269 (current-buffer)))) 270 (display-warning (list 'flymake sublog) 271 (apply #'format-message msg args) 272 (if (numberp level) 273 (or (nth level 274 '(:emergency :error :warning :debug :debug) ) 275 :error) 276 level) 277 "*Flymake log*"))) 278 279 (defun flymake-switch-to-log-buffer () 280 "Go to the *Flymake log* buffer." 281 (interactive) 282 (switch-to-buffer "*Flymake log*")) 283 284 ;;;###autoload 285 (defmacro flymake-log (level msg &rest args) 286 "Log, at level LEVEL, the message MSG formatted with ARGS. 287 LEVEL is passed to `display-warning', which is used to display 288 the warning. If this form is included in a file, 289 the generated warning contains an indication of the file that 290 generated it." 291 (let* ((file (if (fboundp 'macroexp-file-name) 292 (macroexp-file-name) 293 (and (not load-file-name) 294 (bound-and-true-p byte-compile-current-file)))) 295 (sublog (if (stringp file) 296 (intern 297 (file-name-nondirectory 298 (file-name-sans-extension file)))))) 299 `(flymake--log-1 ,level ',sublog ,msg ,@args))) 300 301 (defun flymake-error (text &rest args) 302 "Format TEXT with ARGS and signal an error for Flymake." 303 (let ((msg (apply #'format-message text args))) 304 (flymake-log :error msg) 305 (error (concat "[Flymake] " msg)))) 306 307 (cl-defstruct (flymake--diag 308 (:constructor flymake--diag-make)) 309 locus beg end type text backend data overlay-properties overlay 310 ;; FIXME: See usage of these two in `flymake--highlight-line'. 311 ;; Ideally they wouldn't be needed. 312 orig-beg orig-end) 313 314 ;;;###autoload 315 (defun flymake-make-diagnostic (locus 316 beg 317 end 318 type 319 text 320 &optional data 321 overlay-properties) 322 "Make a Flymake diagnostic for LOCUS's region from BEG to END. 323 LOCUS is a buffer object or a string designating a file name. 324 325 TYPE is a diagnostic symbol and TEXT is string describing the 326 problem detected in this region. DATA is any object that the 327 caller wishes to attach to the created diagnostic for later 328 retrieval with `flymake-diagnostic-data'. 329 330 If LOCUS is a buffer BEG and END should be buffer positions 331 inside it. If LOCUS designates a file, BEG and END should be a 332 cons (LINE . COL) indicating a file position. In this second 333 case, END may be ommited in which case the region is computed 334 using `flymake-diag-region' if the diagnostic is appended to an 335 actual buffer. 336 337 OVERLAY-PROPERTIES is an alist of properties attached to the 338 created diagnostic, overriding the default properties and any 339 properties listed in the `flymake-overlay-control' property of 340 the diagnostic's type symbol." 341 (when (stringp locus) 342 (setq locus (expand-file-name locus))) 343 (flymake--diag-make :locus locus :beg beg :end end 344 :type type :text text :data data 345 :overlay-properties overlay-properties 346 :orig-beg beg 347 :orig-end end)) 348 349 ;;;###autoload 350 (defun flymake-diagnostics (&optional beg end) 351 "Get Flymake diagnostics in region determined by BEG and END. 352 353 If neither BEG or END is supplied, use whole accessible buffer, 354 otherwise if BEG is non-nil and END is nil, consider only 355 diagnostics at BEG." 356 (mapcar (lambda (ov) (overlay-get ov 'flymake-diagnostic)) 357 (flymake--overlays :beg beg :end end))) 358 359 (defmacro flymake--diag-accessor (public internal thing) 360 "Make PUBLIC an alias for INTERNAL, add doc using THING." 361 `(defsubst ,public (diag) 362 ,(format "Get Flymake diagnostic DIAG's %s." (symbol-name thing)) 363 (,internal diag))) 364 365 (flymake--diag-accessor flymake-diagnostic-text flymake--diag-text text) 366 (flymake--diag-accessor flymake-diagnostic-type flymake--diag-type type) 367 (flymake--diag-accessor flymake-diagnostic-backend flymake--diag-backend backend) 368 (flymake--diag-accessor flymake-diagnostic-data flymake--diag-data data) 369 (flymake--diag-accessor flymake-diagnostic-beg flymake--diag-beg beg) 370 (flymake--diag-accessor flymake-diagnostic-end flymake--diag-end end) 371 (flymake--diag-accessor flymake-diagnostic-buffer flymake--diag-locus locus) 372 373 (cl-defun flymake--overlays (&key beg end filter compare key) 374 "Get flymake-related overlays. 375 If BEG is non-nil and END is nil, consider only `overlays-at' 376 BEG. Otherwise consider `overlays-in' the region comprised by BEG 377 and END, defaulting to the whole buffer. Remove all that do not 378 verify FILTER, a function, and sort them by COMPARE (using KEY)." 379 (save-restriction 380 (widen) 381 (let ((ovs (cl-remove-if-not 382 (lambda (ov) 383 (and (overlay-get ov 'flymake-diagnostic) 384 (or (not filter) 385 (funcall filter ov)))) 386 (if (and beg (null end)) 387 (overlays-at beg t) 388 (overlays-in (or beg (point-min)) 389 (or end (point-max))))))) 390 (if compare 391 (cl-sort ovs compare :key (or key 392 #'identity)) 393 ovs)))) 394 395 (defface flymake-error 396 '((((supports :underline (:style wave))) 397 :underline (:style wave :color "Red1")) 398 (t 399 :inherit error)) 400 "Face used for marking error regions." 401 :version "24.4") 402 403 (defface flymake-warning 404 '((((supports :underline (:style wave))) 405 :underline (:style wave :color "deep sky blue")) 406 (t 407 :inherit warning)) 408 "Face used for marking warning regions." 409 :version "24.4") 410 411 (defface flymake-note 412 '((((supports :underline (:style wave))) 413 :underline (:style wave :color "yellow green")) 414 (t 415 :inherit warning)) 416 "Face used for marking note regions." 417 :version "26.1") 418 419 (define-obsolete-face-alias 'flymake-warnline 'flymake-warning "26.1") 420 (define-obsolete-face-alias 'flymake-errline 'flymake-error "26.1") 421 422 ;;;###autoload 423 (defun flymake-diag-region (buffer line &optional col) 424 "Compute BUFFER's region (BEG . END) corresponding to LINE and COL. 425 If COL is nil, return a region just for LINE. Return nil if the 426 region is invalid. This function saves match data." 427 (condition-case-unless-debug _err 428 (with-current-buffer buffer 429 (let ((line (min (max line 1) 430 (line-number-at-pos (point-max) 'absolute)))) 431 (save-excursion 432 (save-match-data 433 (goto-char (point-min)) 434 (forward-line (1- line)) 435 (cl-flet ((fallback-bol 436 () 437 (back-to-indentation) 438 (if (eobp) 439 (line-beginning-position 0) 440 (point))) 441 (fallback-eol 442 (beg) 443 (progn 444 (end-of-line) 445 (skip-chars-backward " \t\f\n" beg) 446 (if (eq (point) beg) 447 (line-beginning-position 2) 448 (point))))) 449 (if (and col (cl-plusp col)) 450 (let* ((beg (progn (forward-char (1- col)) 451 (point))) 452 (sexp-end (or (ignore-errors (end-of-thing 'sexp)) 453 (ignore-errors (end-of-thing 'symbol)))) 454 (end (or (and sexp-end 455 (not (= sexp-end beg)) 456 sexp-end) 457 (and (< (goto-char (1+ beg)) (point-max)) 458 (point))))) 459 (if end 460 (cons beg end) 461 (cons (setq beg (fallback-bol)) 462 (fallback-eol beg)))) 463 (let* ((beg (fallback-bol)) 464 (end (fallback-eol beg))) 465 (cons beg end)))))))) 466 (error (flymake-log :warning "Invalid region line=%s col=%s" line col) 467 nil))) 468 469 (defvar flymake-diagnostic-functions nil 470 "Special hook of Flymake backends that check a buffer. 471 472 The functions in this hook diagnose problems in a buffer's 473 contents and provide information to the Flymake user interface 474 about where and how to annotate problems diagnosed in a buffer. 475 476 Each backend function must be prepared to accept an arbitrary 477 number of arguments: 478 479 * the first argument is always REPORT-FN, a callback function 480 detailed below; 481 482 * the remaining arguments are keyword-value pairs in the 483 form (:KEY VALUE :KEY2 VALUE2...). 484 485 Currently, Flymake may provide these keyword-value pairs: 486 487 * `:recent-changes', a list of recent changes since the last time 488 the backend function was called for the buffer. An empty list 489 indicates that no changes have been recorded. If it is the 490 first time that this backend function is called for this 491 activation of `flymake-mode', then this argument isn't provided 492 at all (i.e. it's not merely nil). 493 494 Each element is in the form (BEG END TEXT) where BEG and END 495 are buffer positions, and TEXT is a string containing the text 496 contained between those positions (if any) after the change was 497 performed. 498 499 * `:changes-start' and `:changes-end', the minimum and maximum 500 buffer positions touched by the recent changes. These are only 501 provided if `:recent-changes' is also provided. 502 503 Whenever Flymake or the user decides to re-check the buffer, 504 backend functions are called as detailed above and are expected 505 to initiate this check, but aren't required to complete it before 506 exiting: if the computation involved is expensive, especially for 507 large buffers, that task can be scheduled for the future using 508 asynchronous processes or other asynchronous mechanisms. 509 510 In any case, backend functions are expected to return quickly or 511 signal an error, in which case the backend is disabled. Flymake 512 will not try disabled backends again for any future checks of 513 this buffer. To reset the list of disabled backends, turn 514 `flymake-mode' off and on again, or interactively call 515 `flymake-start' with a prefix argument. 516 517 If the function returns, Flymake considers the backend to be 518 \"running\". If it has not done so already, the backend is 519 expected to call the function REPORT-FN with a single argument 520 REPORT-ACTION also followed by an optional list of keyword-value 521 pairs in the form (:REPORT-KEY VALUE :REPORT-KEY2 VALUE2...). 522 523 Currently accepted values for REPORT-ACTION are: 524 525 * A (possibly empty) list of diagnostic objects created with 526 `flymake-make-diagnostic', causing Flymake to delete all 527 previous diagnostic annotations in the buffer and create new 528 ones from this list. 529 530 A backend may call REPORT-FN repeatedly in this manner, but 531 only until Flymake considers that the most recently requested 532 buffer check is now obsolete because, say, buffer contents have 533 changed in the meantime. The backend is only given notice of 534 this via a renewed call to the backend function. Thus, to 535 prevent making obsolete reports and wasting resources, backend 536 functions should first cancel any ongoing processing from 537 previous calls. 538 539 * The symbol `:panic', signaling that the backend has encountered 540 an exceptional situation and should be disabled. 541 542 Currently accepted REPORT-KEY arguments are: 543 544 * `:explanation' value should give user-readable details of 545 the situation encountered, if any. 546 547 * `:force': value should be a boolean suggesting that Flymake 548 consider the report even if it was somehow unexpected. 549 550 * `:region': a cons (BEG . END) of buffer positions indicating 551 that the report applies to that region only. Specifically, 552 this means that Flymake will only delete diagnostic annotations 553 of past reports if they intersect the region by at least one 554 character.") 555 556 (put 'flymake-diagnostic-functions 'safe-local-variable #'null) 557 558 (put :error 'flymake-category 'flymake-error) 559 (put :warning 'flymake-category 'flymake-warning) 560 (put :note 'flymake-category 'flymake-note) 561 562 (defvar flymake-diagnostic-types-alist '() "") 563 (make-obsolete-variable 564 'flymake-diagnostic-types-alist 565 "Set properties on the diagnostic symbols instead. See Info 566 Node `(Flymake)Flymake error types'" 567 "27.1") 568 569 (put 'flymake-error 'face 'flymake-error) 570 (put 'flymake-error 'flymake-bitmap 'flymake-error-bitmap) 571 (put 'flymake-error 'severity (warning-numeric-level :error)) 572 (put 'flymake-error 'mode-line-face 'compilation-error) 573 (put 'flymake-error 'flymake-type-name "error") 574 575 (put 'flymake-warning 'face 'flymake-warning) 576 (put 'flymake-warning 'flymake-bitmap 'flymake-warning-bitmap) 577 (put 'flymake-warning 'severity (warning-numeric-level :warning)) 578 (put 'flymake-warning 'mode-line-face 'compilation-warning) 579 (put 'flymake-warning 'flymake-type-name "warning") 580 581 (put 'flymake-note 'face 'flymake-note) 582 (put 'flymake-note 'flymake-bitmap 'flymake-note-bitmap) 583 (put 'flymake-note 'severity (warning-numeric-level :debug)) 584 (put 'flymake-note 'mode-line-face 'compilation-info) 585 (put 'flymake-note 'flymake-type-name "note") 586 587 (defun flymake--lookup-type-property (type prop &optional default) 588 "Look up PROP for diagnostic TYPE. 589 If TYPE doesn't declare PROP in its plist or in the symbol of its 590 associated `flymake-category' return DEFAULT." 591 ;; This function also consults `flymake-diagnostic-types-alist' for 592 ;; backward compatibility. 593 ;; 594 (if (plist-member (symbol-plist type) prop) 595 ;; allow nil values to survive 596 (get type prop) 597 (let (alist) 598 (or 599 (alist-get 600 prop (setq 601 alist 602 (alist-get type flymake-diagnostic-types-alist))) 603 (when-let* ((cat (or 604 (get type 'flymake-category) 605 (alist-get 'flymake-category alist))) 606 (plist (and (symbolp cat) 607 (symbol-plist cat))) 608 (cat-probe (plist-member plist prop))) 609 (cadr cat-probe)) 610 default)))) 611 612 (defun flymake--severity (type) 613 "Get the severity for diagnostic TYPE." 614 (flymake--lookup-type-property type 'severity 615 (warning-numeric-level :error))) 616 617 (defun flymake--fringe-overlay-spec (bitmap &optional recursed) 618 (if (and (symbolp bitmap) 619 (boundp bitmap) 620 (not recursed)) 621 (flymake--fringe-overlay-spec 622 (symbol-value bitmap) t) 623 (and flymake-fringe-indicator-position 624 bitmap 625 (propertize "!" 'display 626 (cons flymake-fringe-indicator-position 627 (if (listp bitmap) 628 bitmap 629 (list bitmap))))))) 630 631 (defun flymake--equal-diagnostic-p (a b) 632 "Tell if A and B are equivalent `flymake--diag' objects." 633 (or (eq a b) 634 (cl-loop for comp in '(flymake--diag-end 635 flymake--diag-beg 636 flymake-diagnostic-type 637 flymake-diagnostic-backend 638 flymake-diagnostic-text) 639 always (equal (funcall comp a) (funcall comp b))))) 640 641 (cl-defun flymake--highlight-line (diagnostic &optional foreign) 642 "Attempt to overlay DIAGNOSTIC in current buffer. 643 644 FOREIGN says if DIAGNOSTIC is \"foreign\" to the current buffer, 645 i.e. managed by another buffer where `flymake-mode' is also 646 active. 647 648 This function mayskip overlay creation if a diagnostic which is 649 the same as DIAGNOSTIC is already highlighted 650 (in the sense of `flymake--equal-diagnostic-p'). In that case 651 the action to take depends on FOREIGN. If nil the existing 652 overlay is deleted, else no overlay is created. 653 654 Return nil or the overlay created." 655 (let* ((type (or (flymake-diagnostic-type diagnostic) 656 :error)) 657 (beg (flymake--diag-beg diagnostic)) 658 (end (flymake--diag-end diagnostic)) 659 (convert (lambda (cell) 660 (flymake-diag-region (current-buffer) 661 (car cell) 662 (cdr cell)))) 663 ov) 664 ;; Convert (LINE . COL) forms of `flymake--diag-beg' and 665 ;; `flymake--diag-end'. Record the converted positions. 666 ;; 667 (cond ((and (consp beg) (not (null end))) 668 (setq beg (car (funcall convert beg))) 669 (when (consp end) 670 (setq end (car (funcall convert end))))) 671 ((consp beg) 672 (cl-destructuring-bind (a . b) (funcall convert beg) 673 (setq beg a end b)))) 674 (setf (flymake--diag-beg diagnostic) beg 675 (flymake--diag-end diagnostic) end) 676 ;; Try to fix the remedy the situation if there is the same 677 ;; diagnostic is already registered in the same place, which only 678 ;; happens for clashes between domestic and foreign diagnostics 679 (cl-loop for e in (flymake-diagnostics beg end) 680 when (flymake--equal-diagnostic-p e diagnostic) 681 ;; FIXME. This is an imperfect heuristic. Ideally, we'd 682 ;; want to delete no overlays and keep annotating the 683 ;; superseded foreign in an overlay but hide it from most 684 ;; `flymake-diagnostics' calls. If the target buffer is 685 ;; killed we can keep the "latent" state of the foreign 686 ;; diagnostic (with filename and updated line/col info). 687 ;; If it is revisited the foreign diagnostic can be 688 ;; revived again. 689 do (if foreign 690 (cl-return-from flymake--highlight-line nil) 691 (setf (flymake--diag-beg e) 692 (flymake--diag-orig-beg e) 693 (flymake--diag-end e) 694 (flymake--diag-orig-end e)) 695 (delete-overlay (flymake--diag-overlay e)))) 696 (setq ov (make-overlay end beg)) 697 (setf (flymake--diag-beg diagnostic) (overlay-start ov) 698 (flymake--diag-end diagnostic) (overlay-end ov)) 699 ;; First set `category' in the overlay 700 ;; 701 (overlay-put ov 'category 702 (flymake--lookup-type-property type 'flymake-category)) 703 ;; Now "paint" the overlay with all the other non-category 704 ;; properties. 705 (cl-loop 706 for (ov-prop . value) in 707 (append (reverse 708 (flymake--diag-overlay-properties diagnostic)) 709 (reverse ; ensure earlier props override later ones 710 (flymake--lookup-type-property type 'flymake-overlay-control)) 711 (alist-get type flymake-diagnostic-types-alist)) 712 do (overlay-put ov ov-prop value)) 713 ;; Now ensure some essential defaults are set 714 ;; 715 (cl-flet ((default-maybe 716 (prop value) 717 (unless (plist-member (overlay-properties ov) prop) 718 (overlay-put ov prop (flymake--lookup-type-property 719 type prop value))))) 720 (default-maybe 'face 'flymake-error) 721 (default-maybe 'before-string 722 (flymake--fringe-overlay-spec 723 (flymake--lookup-type-property 724 type 725 'flymake-bitmap 726 (alist-get 'bitmap (alist-get type ; backward compat 727 flymake-diagnostic-types-alist))))) 728 (default-maybe 'help-echo 729 (lambda (window _ov pos) 730 (with-selected-window window 731 (mapconcat 732 #'flymake-diagnostic-text 733 (flymake-diagnostics pos) 734 "\n")))) 735 (default-maybe 'severity (warning-numeric-level :error)) 736 ;; Use (PRIMARY . SECONDARY) priority, to avoid clashing with 737 ;; `region' face, for example (bug#34022). 738 (default-maybe 'priority (cons nil (+ 40 (overlay-get ov 'severity))))) 739 ;; Some properties can't be overridden. 740 ;; 741 (overlay-put ov 'evaporate t) 742 (overlay-put ov 'flymake-diagnostic diagnostic) 743 (setf (flymake--diag-overlay diagnostic) ov) 744 ov)) 745 746 ;; Nothing in Flymake uses this at all any more, so this is just for 747 ;; third-party compatibility. 748 (define-obsolete-function-alias 'flymake-display-warning 'message-box "26.1") 749 750 (defvar-local flymake--state nil 751 "State of a buffer's multiple Flymake backends. 752 The keys to this hash table are functions as found in 753 `flymake-diagnostic-functions'. The values are structures 754 of the type `flymake--state', with these slots: 755 756 `running', a symbol to keep track of a backend's replies via its 757 REPORT-FN argument. A backend is running if this key is 758 present. If nil, Flymake isn't expecting any replies from the 759 backend. 760 761 `diags', a (possibly empty) list of recent diagnostic objects 762 created by the backend with `flymake-make-diagnostic'. 763 764 `reported-p', a boolean indicating if the backend has replied 765 since it last was contacted. 766 767 `disabled', a string with the explanation for a previous 768 exceptional situation reported by the backend, nil if the 769 backend is operating normally. 770 771 `foreign-diags', a hash table of buffers/files to 772 collections of diagnostics outside the buffer where this 773 `flymake--state' pertains.") 774 775 (cl-defstruct (flymake--state 776 (:constructor flymake--make-backend-state)) 777 running reported-p disabled diags (foreign-diags 778 (make-hash-table))) 779 780 (defmacro flymake--with-backend-state (backend state-var &rest body) 781 "Bind BACKEND's STATE-VAR to its state, run BODY." 782 (declare (indent 2) (debug (sexp sexp &rest form))) 783 (let ((b (make-symbol "b"))) 784 `(let* ((,b ,backend) 785 (,state-var 786 (or (gethash ,b flymake--state) 787 (puthash ,b (flymake--make-backend-state) 788 flymake--state)))) 789 ,@body))) 790 791 (defun flymake-is-running () 792 "Tell if Flymake has running backends in this buffer." 793 (flymake-running-backends)) 794 795 ;; FIXME: clone of `isearch-intersects-p'! Make this an util. 796 (defun flymake--intersects-p (start0 end0 start1 end1) 797 "Return t if regions START0..END0 and START1..END1 intersect." 798 (or (and (>= start0 start1) (< start0 end1)) 799 (and (> end0 start1) (<= end0 end1)) 800 (and (>= start1 start0) (< start1 end0)) 801 (and (> end1 start0) (<= end1 end0)))) 802 803 (cl-defun flymake--handle-report 804 (backend token report-action 805 &key explanation force region 806 &allow-other-keys 807 &aux 808 (state (or (gethash backend flymake--state) 809 (error "Can't find state for %s in `flymake--state'" 810 backend))) 811 expected-token) 812 "Handle reports from BACKEND identified by TOKEN. 813 BACKEND, REPORT-ACTION and EXPLANATION, and FORCE conform to the 814 calling convention described in 815 `flymake-diagnostic-functions' (which see). Optional FORCE says 816 to handle a report even if TOKEN was not expected. REGION is 817 a (BEG . END) pair of buffer positions indicating that this 818 report applies to that region." 819 (cond 820 ((null state) 821 (flymake-error 822 "Unexpected report from unknown backend %s" backend)) 823 ((flymake--state-disabled state) 824 (flymake-error 825 "Unexpected report from disabled backend %s" backend)) 826 ((progn 827 (setq expected-token (flymake--state-running state)) 828 (null expected-token)) 829 ;; should never happen 830 (flymake-error "Unexpected report from stopped backend %s" backend)) 831 ((not (or (eq expected-token token) 832 force)) 833 (flymake-error "Obsolete report from backend %s with explanation %s" 834 backend explanation)) 835 ((eq :panic report-action) 836 (flymake--disable-backend backend explanation)) 837 ((not (listp report-action)) 838 (flymake--disable-backend backend 839 (format "Unknown action %S" report-action)) 840 (flymake-error "Expected report, but got unknown key %s" report-action)) 841 (t 842 (flymake--publish-diagnostics report-action 843 :backend backend 844 :state state 845 :region region) 846 (when flymake-check-start-time 847 (flymake-log :debug "backend %s reported %d diagnostics in %.2f second(s)" 848 backend 849 (length report-action) 850 (float-time 851 (time-since flymake-check-start-time)))))) 852 (setf (flymake--state-reported-p state) t) 853 (flymake--update-diagnostics-listings (current-buffer))) 854 855 (defun flymake--clear-foreign-diags (state) 856 (maphash (lambda (_buffer diags) 857 (cl-loop for d in diags 858 when (flymake--diag-overlay d) 859 do (delete-overlay it))) 860 (flymake--state-foreign-diags state)) 861 (clrhash (flymake--state-foreign-diags state))) 862 863 (defvar-local flymake-mode nil) 864 865 (cl-defun flymake--publish-diagnostics (diags &key backend state region) 866 "Helper for `flymake--handle-report'. 867 Publish DIAGS, which contain diagnostics for the current buffer 868 and other buffers." 869 (dolist (d diags) (setf (flymake--diag-backend d) backend)) 870 (save-restriction 871 (widen) 872 ;; First, clean up. Remove diagnostics from bookeeping lists and 873 ;; their overlays from buffers. 874 ;; 875 (cond 876 (;; If there is a `region' arg, only affect the diagnostics whose 877 ;; overlays are in a certain region. Discard "foreign" 878 ;; diagnostics. 879 region 880 (cl-loop for diag in (flymake--state-diags state) 881 for ov = (flymake--diag-overlay diag) 882 if (or (not (overlay-buffer ov)) 883 (flymake--intersects-p 884 (overlay-start ov) (overlay-end ov) 885 (car region) (cdr region))) 886 do (delete-overlay ov) 887 else collect diag into surviving 888 finally (setf (flymake--state-diags state) 889 surviving))) 890 (;; Else, if this is the first report, zero all lists and delete 891 ;; all associated overlays. 892 (not (flymake--state-reported-p state)) 893 (cl-loop for diag in (flymake--state-diags state) 894 for ov = (flymake--diag-overlay diag) 895 when ov do (delete-overlay ov)) 896 (setf (flymake--state-diags state) nil) 897 ;; Also clear all overlays for `foreign-diags' in all other 898 ;; buffers. 899 (flymake--clear-foreign-diags state)) 900 (;; If this is not the first report, do no cleanup. 901 t)) 902 903 ;; Now place new overlays for all diagnostics: "domestic" 904 ;; diagnostics are for the current buffer; "foreign" may be for a 905 ;; some other live buffer or for a file name that hasn't a buffer 906 ;; yet. If a foreign diagnostic is for a buffer, convert to a 907 ;; file name, protecting it against that buffer's killing. 908 ;; 909 (cl-loop 910 for d in diags 911 for locus = (flymake--diag-locus d) 912 do (cond ((eq locus (current-buffer)) 913 (push d (flymake--state-diags state)) 914 (flymake--highlight-line d)) 915 (t 916 (when (or (buffer-live-p locus) 917 (setq locus (find-buffer-visiting locus))) 918 (with-current-buffer locus 919 (when flymake-mode (flymake--highlight-line d 'foreign)) 920 ;; Ensure locus of a foreign diag is always a file-name 921 ;; string, even if created from a buffer. 922 (setf (flymake--diag-locus d) (buffer-file-name)))) 923 (cl-assert (stringp (flymake--diag-locus d))) 924 (push d (gethash (flymake--diag-locus d) 925 (flymake--state-foreign-diags state)))))))) 926 927 (defun flymake-make-report-fn (backend &optional token) 928 "Make a suitable anonymous report function for BACKEND. 929 BACKEND is used to help Flymake distinguish different diagnostic 930 sources. If provided, TOKEN helps Flymake distinguish between 931 different runs of the same backend." 932 (let ((buffer (current-buffer))) 933 (lambda (&rest args) 934 (when (buffer-live-p buffer) 935 (with-current-buffer buffer 936 (apply #'flymake--handle-report backend token args)))))) 937 938 (defun flymake--collect (fn &optional message-prefix) 939 "Collect Flymake backends matching FN. 940 If MESSAGE-PREFIX, echo a message using that prefix." 941 (unless flymake--state 942 (user-error "Flymake is not initialized")) 943 (let (retval) 944 (maphash (lambda (backend state) 945 (when (funcall fn state) (push backend retval))) 946 flymake--state) 947 (when message-prefix 948 (message "%s%s" 949 message-prefix 950 (mapconcat (lambda (s) (format "%s" s)) 951 retval ", "))) 952 retval)) 953 954 (defun flymake-running-backends () 955 "Compute running Flymake backends in current buffer." 956 (interactive) 957 (flymake--collect #'flymake--state-running 958 (and (called-interactively-p 'interactive) 959 "Running backends: "))) 960 961 (defun flymake-disabled-backends () 962 "Compute disabled Flymake backends in current buffer." 963 (interactive) 964 (flymake--collect #'flymake--state-disabled 965 (and (called-interactively-p 'interactive) 966 "Disabled backends: "))) 967 968 (defun flymake-reporting-backends () 969 "Compute reporting Flymake backends in current buffer." 970 (interactive) 971 (flymake--collect #'flymake--state-reported-p 972 (and (called-interactively-p 'interactive) 973 "Reporting backends: "))) 974 975 (defun flymake--disable-backend (backend &optional explanation) 976 "Disable BACKEND because EXPLANATION. 977 If it is running also stop it." 978 (flymake-log :warning "Disabling backend %s because %s" backend explanation) 979 (flymake--with-backend-state backend state 980 (setf (flymake--state-running state) nil 981 (flymake--state-disabled state) explanation 982 (flymake--state-reported-p state) t))) 983 984 (defun flymake--run-backend (backend &optional args) 985 "Run the backend BACKEND, re-enabling if necessary. 986 ARGS is a keyword-value plist passed to the backend along 987 with a report function." 988 (flymake-log :debug "Running backend %s" backend) 989 (let ((run-token (cl-gensym "backend-token"))) 990 (flymake--with-backend-state backend state 991 (setf (flymake--state-running state) run-token 992 (flymake--state-disabled state) nil 993 (flymake--state-reported-p state) nil)) 994 ;; FIXME: Should use `condition-case-unless-debug' here, but don't 995 ;; for two reasons: (1) that won't let me catch errors from inside 996 ;; `ert-deftest' where `debug-on-error' appears to be always 997 ;; t. (2) In cases where the user is debugging elisp somewhere 998 ;; else, and using flymake, the presence of a frequently 999 ;; misbehaving backend in the global hook (most likely the legacy 1000 ;; backend) will trigger an annoying backtrace. 1001 ;; 1002 (condition-case err 1003 (apply backend (flymake-make-report-fn backend run-token) 1004 args) 1005 (error 1006 (flymake--disable-backend backend err))))) 1007 1008 (defvar-local flymake--recent-changes nil 1009 "Recent changes collected by `flymake-after-change-function'.") 1010 (defvar flymake-mode) 1011 1012 (defun flymake-start (&optional deferred force) 1013 "Start a syntax check for the current buffer. 1014 DEFERRED is a list of symbols designating conditions to wait for 1015 before actually starting the check. If it is nil (the list is 1016 empty), start it immediately, else defer the check to when those 1017 conditions are met. Currently recognized conditions are 1018 `post-command', for waiting until the current command is over, 1019 `on-display', for waiting until the buffer is actually displayed 1020 in a window. If DEFERRED is t, wait for all known conditions. 1021 1022 With optional FORCE run even disabled backends. 1023 1024 Interactively, with a prefix arg, FORCE is t." 1025 (interactive (list nil current-prefix-arg)) 1026 (let ((deferred (if (eq t deferred) 1027 '(post-command on-display) 1028 deferred)) 1029 (buffer (current-buffer))) 1030 (cl-labels 1031 ((start-post-command 1032 () 1033 (remove-hook 'post-command-hook #'start-post-command 1034 nil) 1035 ;; The buffer may have disappeared already, e.g. because of 1036 ;; code like `(with-temp-buffer (python-mode) ...)'. 1037 (when (buffer-live-p buffer) 1038 (with-current-buffer buffer 1039 (flymake-start (remove 'post-command deferred) force)))) 1040 (start-on-display 1041 () 1042 (remove-hook 'window-configuration-change-hook #'start-on-display 1043 'local) 1044 (flymake-start (remove 'on-display deferred) force))) 1045 (cond ((and (memq 'post-command deferred) 1046 this-command) 1047 (add-hook 'post-command-hook 1048 #'start-post-command 1049 'append nil)) 1050 ((and (memq 'on-display deferred) 1051 (not (get-buffer-window (current-buffer)))) 1052 (add-hook 'window-configuration-change-hook 1053 #'start-on-display 1054 'append 'local)) 1055 (flymake-mode 1056 (setq flymake-check-start-time (float-time)) 1057 (let ((backend-args 1058 (and 1059 flymake--recent-changes 1060 (list :recent-changes 1061 flymake--recent-changes 1062 :changes-start 1063 (cl-reduce 1064 #'min (mapcar #'car flymake--recent-changes)) 1065 :changes-end 1066 (cl-reduce 1067 #'max (mapcar #'cadr flymake--recent-changes)))))) 1068 (setq flymake--recent-changes nil) 1069 (run-hook-wrapped 1070 'flymake-diagnostic-functions 1071 (lambda (backend) 1072 (cond 1073 ((and (not force) 1074 (flymake--with-backend-state backend state 1075 (flymake--state-disabled state))) 1076 (flymake-log :debug "Backend %s is disabled, not starting" 1077 backend)) 1078 (t 1079 (flymake--run-backend backend backend-args))) 1080 nil)))))))) 1081 1082 (defvar flymake-mode-map 1083 (let ((map (make-sparse-keymap))) map) 1084 "Keymap for `flymake-mode'.") 1085 1086 ;;;###autoload 1087 (define-minor-mode flymake-mode 1088 "Toggle Flymake mode on or off. 1089 1090 Flymake is an Emacs minor mode for on-the-fly syntax checking. 1091 Flymake collects diagnostic information from multiple sources, 1092 called backends, and visually annotates the buffer with the 1093 results. 1094 1095 Flymake performs these checks while the user is editing. 1096 The customization variables `flymake-start-on-flymake-mode', 1097 `flymake-no-changes-timeout' determine the exact circumstances 1098 whereupon Flymake decides to initiate a check of the buffer. 1099 1100 The commands `flymake-goto-next-error' and 1101 `flymake-goto-prev-error' can be used to navigate among Flymake 1102 diagnostics annotated in the buffer. 1103 1104 The visual appearance of each type of diagnostic can be changed 1105 by setting properties `flymake-overlay-control', `flymake-bitmap' 1106 and `flymake-severity' on the symbols of diagnostic types (like 1107 `:error', `:warning' and `:note'). 1108 1109 Activation or deactivation of backends used by Flymake in each 1110 buffer happens via the special hook 1111 `flymake-diagnostic-functions'. 1112 1113 Some backends may take longer than others to respond or complete, 1114 and some may decide to disable themselves if they are not 1115 suitable for the current buffer. The commands 1116 `flymake-running-backends', `flymake-disabled-backends' and 1117 `flymake-reporting-backends' summarize the situation, as does the 1118 special *Flymake log* buffer." :group 'flymake :lighter 1119 flymake-mode-line-format :keymap flymake-mode-map 1120 (cond 1121 ;; Turning the mode ON. 1122 (flymake-mode 1123 (add-hook 'after-change-functions 'flymake-after-change-function nil t) 1124 (add-hook 'after-save-hook 'flymake-after-save-hook nil t) 1125 (add-hook 'kill-buffer-hook 'flymake-kill-buffer-hook nil t) 1126 (add-hook 'eldoc-documentation-functions 'flymake-eldoc-function t t) 1127 1128 ;; If Flymake happened to be already already ON, we must cleanup 1129 ;; existing diagnostic overlays, lest we forget them by blindly 1130 ;; reinitializing `flymake--state' in the next line. 1131 ;; See https://github.com/joaotavora/eglot/issues/223. 1132 (mapc #'delete-overlay (flymake--overlays)) 1133 (setq flymake--state (make-hash-table)) 1134 (setq flymake--recent-changes nil) 1135 1136 (when flymake-start-on-flymake-mode (flymake-start t)) 1137 1138 ;; Other diagnostic sources may already target this buffer's file 1139 ;; before we turned on: these sources may be of two types... 1140 (let ((source (current-buffer)) 1141 (bfn buffer-file-name)) 1142 ;; 1. For `flymake-list-only-diagnostics': here, we do nothing. 1143 ;; FIXME: We could remove the corresponding entry from that 1144 ;; variable, as we assume that new diagnostics will come in soon 1145 ;; via the brand new `flymake-mode' setup. For simplicity's 1146 ;; sake, we have opted to leave the backend for now. 1147 nil 1148 ;; 2. other buffers where a backend has created "foreign" 1149 ;; diagnostics and pointed them here. We must highlight them in 1150 ;; this buffer, i.e. create overlays for them. Those other 1151 ;; buffers and backends are still responsible for them, i.e. the 1152 ;; current buffer does not "own" these foreign diags. 1153 (dolist (buffer (buffer-list)) 1154 (with-current-buffer buffer 1155 (when (and flymake-mode flymake--state) 1156 (maphash (lambda (_backend state) 1157 (maphash (lambda (file diags) 1158 (when (or (eq file source) 1159 (string= bfn (expand-file-name file))) 1160 (with-current-buffer source 1161 (mapc (lambda (diag) 1162 (flymake--highlight-line diag 1163 'foreign)) 1164 diags)))) 1165 (flymake--state-foreign-diags state))) 1166 flymake--state)))))) 1167 1168 ;; Turning the mode OFF. 1169 (t 1170 (remove-hook 'after-change-functions 'flymake-after-change-function t) 1171 (remove-hook 'after-save-hook 'flymake-after-save-hook t) 1172 (remove-hook 'kill-buffer-hook 'flymake-kill-buffer-hook t) 1173 ;;+(remove-hook 'find-file-hook (function flymake-find-file-hook) t) 1174 (remove-hook 'eldoc-documentation-functions 'flymake-eldoc-function t) 1175 1176 (when flymake-timer 1177 (cancel-timer flymake-timer) 1178 (setq flymake-timer nil)) 1179 (mapc #'delete-overlay (flymake--overlays)) 1180 (when flymake--state 1181 (maphash (lambda (_backend state) 1182 (flymake--clear-foreign-diags state)) 1183 flymake--state))) 1184 ;; turning Flymake on or off has consequences for listings 1185 (flymake--update-diagnostics-listings (current-buffer)))) 1186 1187 (defun flymake--schedule-timer-maybe () 1188 "(Re)schedule an idle timer for checking the buffer. 1189 Do it only if `flymake-no-changes-timeout' is non-nil." 1190 (when flymake-timer (cancel-timer flymake-timer)) 1191 (when flymake-no-changes-timeout 1192 (setq 1193 flymake-timer 1194 (run-with-idle-timer 1195 ;; This can use time-convert instead of seconds-to-time, 1196 ;; once we can assume Emacs 27 or later. 1197 (seconds-to-time flymake-no-changes-timeout) 1198 nil 1199 (lambda (buffer) 1200 (when (buffer-live-p buffer) 1201 (with-current-buffer buffer 1202 (when (and flymake-mode 1203 flymake-no-changes-timeout) 1204 (flymake-log 1205 :debug "starting syntax check after idle for %s seconds" 1206 flymake-no-changes-timeout) 1207 (flymake-start t)) 1208 (setq flymake-timer nil)))) 1209 (current-buffer))))) 1210 1211 ;;;###autoload 1212 (defun flymake-mode-on () 1213 "Turn Flymake mode on." 1214 (flymake-mode 1)) 1215 1216 ;;;###autoload 1217 (defun flymake-mode-off () 1218 "Turn Flymake mode off." 1219 (flymake-mode 0)) 1220 1221 (make-obsolete 'flymake-mode-on 'flymake-mode "26.1") 1222 (make-obsolete 'flymake-mode-off 'flymake-mode "26.1") 1223 1224 (defun flymake-after-change-function (start stop _len) 1225 "Start syntax check for current buffer if it isn't already running. 1226 START and STOP and LEN are as in `after-change-functions'." 1227 (let((new-text (buffer-substring start stop))) 1228 (push (list start stop new-text) flymake--recent-changes) 1229 (flymake--schedule-timer-maybe))) 1230 1231 (defun flymake-after-save-hook () 1232 (when flymake-start-on-save-buffer 1233 (flymake-log :debug "starting syntax check as buffer was saved") 1234 (flymake-start t))) 1235 1236 (defun flymake-kill-buffer-hook () 1237 ;; Explicitly set flymake off, because that does a lot of useful 1238 ;; cleanup. 1239 (flymake-mode -1)) 1240 1241 (defun flymake-find-file-hook () 1242 (unless (or flymake-mode 1243 (null flymake-diagnostic-functions)) 1244 (flymake-mode) 1245 (flymake-log :warning "Turned on in `flymake-find-file-hook'"))) 1246 1247 (defun flymake-eldoc-function (report-doc &rest _) 1248 "Document diagnostics at point. 1249 Intended for `eldoc-documentation-functions' (which see)." 1250 (let ((diags (flymake-diagnostics (point)))) 1251 (when diags 1252 (funcall report-doc 1253 (mapconcat #'flymake-diagnostic-text diags "\n"))))) 1254 1255 (defun flymake-goto-next-error (&optional n filter interactive) 1256 "Go to Nth next Flymake diagnostic that matches FILTER. 1257 Interactively, always move to the next diagnostic. With a prefix 1258 arg, skip any diagnostics with a severity less than `:warning'. 1259 1260 If `flymake-wrap-around' is non-nil and no more next diagnostics, 1261 resumes search from top. 1262 1263 FILTER is a list of diagnostic types. Only diagnostics with 1264 matching severities matching are considered. If nil (the 1265 default) no filter is applied." 1266 ;; TODO: let filter be a number, a severity below which diags are 1267 ;; skipped. 1268 (interactive (list 1 1269 (if current-prefix-arg 1270 '(:error :warning)) 1271 t)) 1272 (let* ((n (or n 1)) 1273 (ovs (flymake--overlays :filter 1274 (lambda (ov) 1275 (let ((diag (overlay-get 1276 ov 1277 'flymake-diagnostic))) 1278 (and diag 1279 (or 1280 (not filter) 1281 (cl-find 1282 (flymake--severity 1283 (flymake-diagnostic-type diag)) 1284 filter :key #'flymake--severity))))) 1285 :compare (if (cl-plusp n) #'< #'>) 1286 :key #'overlay-start)) 1287 (tail (cl-member-if (lambda (ov) 1288 (if (cl-plusp n) 1289 (> (overlay-start ov) 1290 (point)) 1291 (< (overlay-start ov) 1292 (point)))) 1293 ovs)) 1294 (chain (if flymake-wrap-around 1295 (if tail 1296 (progn (setcdr (last tail) ovs) tail) 1297 (and ovs (setcdr (last ovs) ovs))) 1298 tail)) 1299 (target (nth (1- n) chain))) 1300 (cond (target 1301 (goto-char (overlay-start target)) 1302 (when interactive 1303 (message 1304 "%s" 1305 (funcall (overlay-get target 'help-echo) 1306 (selected-window) target (point))))) 1307 (interactive 1308 (user-error "No more Flymake diagnostics%s" 1309 (if filter 1310 (format " of %s severity" 1311 (mapconcat #'symbol-name filter ", ")) "")))))) 1312 1313 (defun flymake-goto-prev-error (&optional n filter interactive) 1314 "Go to Nth previous Flymake diagnostic that matches FILTER. 1315 Interactively, always move to the previous diagnostic. With a 1316 prefix arg, skip any diagnostics with a severity less than 1317 `:warning'. 1318 1319 If `flymake-wrap-around' is non-nil and no more previous 1320 diagnostics, resumes search from bottom. 1321 1322 FILTER is a list of diagnostic types. Only diagnostics with 1323 matching severities matching are considered. If nil (the 1324 default) no filter is applied." 1325 (interactive (list 1 (if current-prefix-arg 1326 '(:error :warning)) 1327 t)) 1328 (flymake-goto-next-error (- (or n 1)) filter interactive)) 1329 1330 1331 ;;; Mode-line and menu 1332 ;;; 1333 (easy-menu-define flymake-menu flymake-mode-map "Flymake" 1334 '("Flymake" 1335 [ "Go to next problem" flymake-goto-next-error t ] 1336 [ "Go to previous problem" flymake-goto-prev-error t ] 1337 [ "Check now" flymake-start t ] 1338 [ "List all problems" flymake-show-diagnostics-buffer t ] 1339 "--" 1340 [ "Go to log buffer" flymake-switch-to-log-buffer t ] 1341 [ "Turn off Flymake" flymake-mode t ])) 1342 1343 (defcustom flymake-mode-line-format 1344 '(" " flymake-mode-line-title flymake-mode-line-exception 1345 flymake-mode-line-counters) 1346 "Mode line construct for customizing Flymake information." 1347 :type '(repeat (choice string symbol))) 1348 1349 (defcustom flymake-mode-line-counter-format 1350 '("[" 1351 flymake-mode-line-error-counter 1352 flymake-mode-line-warning-counter 1353 flymake-mode-line-note-counter "]") 1354 "Mode-line construct for formatting Flymake diagnostic counters. 1355 This is a suitable place for placing the `flymake-error-counter', 1356 `flymake-warning-counter' and `flymake-note-counter' constructs. 1357 Separating each of these with space is not necessary." 1358 :type '(repeat (choice string symbol))) 1359 1360 (defvar flymake-mode-line-title '(:eval (flymake--mode-line-title)) 1361 "Mode-line construct to show Flymake's mode name and menu.") 1362 1363 (defvar flymake-mode-line-exception '(:eval (flymake--mode-line-exception)) 1364 "Mode-line construct to report on exceptional Flymake status.") 1365 1366 (defvar flymake-mode-line-counters '(:eval (flymake--mode-line-counters)) 1367 "Mode-line construct for counting Flymake diagnostics. 1368 The counters are only placed if some Flymake backend initialized 1369 correctly.") 1370 1371 (defvar flymake-mode-line-error-counter 1372 `(:eval (flymake--mode-line-counter :error t))) 1373 (defvar flymake-mode-line-warning-counter 1374 `(:eval (flymake--mode-line-counter :warning))) 1375 (defvar flymake-mode-line-note-counter 1376 `(:eval (flymake--mode-line-counter :note))) 1377 1378 (put 'flymake-mode-line-format 'risky-local-variable t) 1379 (put 'flymake-mode-line-title 'risky-local-variable t) 1380 (put 'flymake-mode-line-exception 'risky-local-variable t) 1381 (put 'flymake-mode-line-counters 'risky-local-variable t) 1382 (put 'flymake-mode-line-error-counter 'risky-local-variable t) 1383 (put 'flymake-mode-line-warning-counter 'risky-local-variable t) 1384 (put 'flymake-mode-line-note-counter 'risky-local-variable t) 1385 1386 (defun flymake--mode-line-title () 1387 `(:propertize 1388 "Flymake" 1389 mouse-face mode-line-highlight 1390 help-echo 1391 ,(lambda (&rest _) 1392 (concat 1393 (format "%s known backends\n" (hash-table-count flymake--state)) 1394 (format "%s running\n" (length (flymake-running-backends))) 1395 (format "%s disabled\n" (length (flymake-disabled-backends))) 1396 "mouse-1: Display minor mode menu\n" 1397 "mouse-2: Show help for minor mode")) 1398 keymap 1399 ,(let ((map (make-sparse-keymap))) 1400 (define-key map [mode-line down-mouse-1] 1401 flymake-menu) 1402 (define-key map [mode-line down-mouse-3] 1403 flymake-menu) 1404 (define-key map [mode-line mouse-2] 1405 (lambda () 1406 (interactive) 1407 (describe-function 'flymake-mode))) 1408 map))) 1409 1410 (defun flymake--mode-line-exception () 1411 "Helper for `flymake-mode-line-exception'." 1412 (pcase-let* ((running) (reported) 1413 (`(,ind ,face ,explain) 1414 (cond ((zerop (hash-table-count flymake--state)) 1415 '("?" nil "No known backends")) 1416 ((cl-set-difference 1417 (setq running (flymake-running-backends)) 1418 (setq reported (flymake-reporting-backends))) 1419 `("Wait" compilation-mode-line-run 1420 ,(format "Waiting for %s running backend(s)" 1421 (length (cl-set-difference running reported))))) 1422 ((and (flymake-disabled-backends) (null running)) 1423 '("!" compilation-mode-line-run 1424 "All backends disabled")) 1425 (t 1426 '(nil nil nil))))) 1427 (when ind 1428 `(":" 1429 (:propertize ,ind face ,face 1430 help-echo ,explain 1431 keymap ,(let ((map (make-sparse-keymap))) 1432 (define-key map [mode-line mouse-1] 1433 'flymake-switch-to-log-buffer) 1434 map)))))) 1435 1436 (defun flymake--mode-line-counters () 1437 (when (flymake-running-backends) flymake-mode-line-counter-format)) 1438 1439 (defun flymake--mode-line-counter (type &optional no-space) 1440 "Compute number of diagnostics in buffer with TYPE's severity. 1441 TYPE is usually keyword `:error', `:warning' or `:note'." 1442 (let ((count 0) 1443 (face (flymake--lookup-type-property type 1444 'mode-line-face 1445 'compilation-error))) 1446 (dolist (d (flymake-diagnostics)) 1447 (when (= (flymake--severity type) 1448 (flymake--severity (flymake-diagnostic-type d))) 1449 (cl-incf count))) 1450 (when (or (cl-plusp count) 1451 (cond ((eq flymake-suppress-zero-counters t) 1452 nil) 1453 (flymake-suppress-zero-counters 1454 (>= (flymake--severity type) 1455 (warning-numeric-level 1456 flymake-suppress-zero-counters))) 1457 (t t))) 1458 `(,(if no-space "" '(:propertize " ")) 1459 (:propertize 1460 ,(format "%d" count) 1461 face ,face 1462 mouse-face mode-line-highlight 1463 keymap 1464 ,(let ((map (make-sparse-keymap))) 1465 (define-key map (vector 'mode-line 1466 mouse-wheel-down-event) 1467 (lambda (event) 1468 (interactive "e") 1469 (with-selected-window (posn-window (event-start event)) 1470 (flymake-goto-prev-error 1 (list type) t)))) 1471 (define-key map (vector 'mode-line 1472 mouse-wheel-up-event) 1473 (lambda (event) 1474 (interactive "e") 1475 (with-selected-window (posn-window (event-start event)) 1476 (flymake-goto-next-error 1 (list type) t)))) 1477 map)))))) 1478 1479 ;;; Per-buffer diagnostic listing 1480 1481 (defvar-local flymake--diagnostics-buffer-source nil) 1482 1483 (defvar flymake-diagnostics-buffer-mode-map 1484 (let ((map (make-sparse-keymap))) 1485 (define-key map (kbd "RET") 'flymake-goto-diagnostic) 1486 (define-key map (kbd "SPC") 'flymake-show-diagnostic) 1487 map)) 1488 1489 (defun flymake-show-diagnostic (pos &optional other-window) 1490 "Show location of diagnostic at POS." 1491 (interactive (list (point) t)) 1492 (let* ((id (or (tabulated-list-get-id pos) 1493 (user-error "Nothing at point"))) 1494 (diag (plist-get id :diagnostic)) 1495 (locus (flymake--diag-locus diag)) 1496 (beg (flymake--diag-beg diag)) 1497 (end (flymake--diag-end diag)) 1498 (visit (lambda (b e) 1499 (goto-char b) 1500 (pulse-momentary-highlight-region (point) 1501 (or e (line-end-position)) 1502 'highlight)))) 1503 (with-current-buffer (cond ((bufferp locus) locus) 1504 (t (find-file-noselect locus))) 1505 (with-selected-window 1506 (display-buffer (current-buffer) other-window) 1507 (cond (;; an annotated diagnostic (most common case), or a 1508 ;; non-annotated buffer diag 1509 (number-or-marker-p beg) 1510 (funcall visit beg end)) 1511 (;; a non-annotated file diag (TODO: could use `end' 1512 ;; here, too) 1513 (pcase-let ((`(,bbeg . ,bend) 1514 (flymake-diag-region (current-buffer) 1515 (car beg) 1516 (cdr beg)))) 1517 (funcall visit bbeg bend))))) 1518 (current-buffer)))) 1519 1520 (defun flymake-goto-diagnostic (pos) 1521 "Show location of diagnostic at POS. 1522 POS can be a buffer position or a button" 1523 (interactive "d") 1524 (pop-to-buffer 1525 (flymake-show-diagnostic (if (button-type pos) (button-start pos) pos)))) 1526 1527 (defun flymake--tabulated-entries-1 (diags project-root) 1528 "Helper for `flymake--diagnostic-buffer-entries'. 1529 PROJECT-ROOT indicates that each entry should be preceded by the 1530 filename of the diagnostic relative to that directory." 1531 (cl-loop 1532 for diag in diags 1533 for locus = (flymake-diagnostic-buffer diag) 1534 for file = (if (bufferp locus) 1535 (buffer-file-name locus) 1536 locus) 1537 for overlay = (flymake--diag-overlay diag) 1538 for (line . col) = 1539 (cond (;; has live overlay, use overlay for position 1540 (and overlay (overlay-buffer overlay)) 1541 (with-current-buffer (overlay-buffer overlay) 1542 (save-excursion 1543 (goto-char (overlay-start overlay)) 1544 (cons (line-number-at-pos) 1545 (- (point) 1546 (line-beginning-position)))))) 1547 (;; diagnostic not annotated, maybe foreign, check for cons 1548 (consp (flymake--diag-beg diag)) 1549 (flymake--diag-beg diag)) 1550 (;; may still be a valid foreign diagnostic 1551 (consp (flymake--diag-orig-beg diag)) 1552 (flymake--diag-orig-beg diag)) 1553 (;; somehow dead annotated diagnostic, ignore/give up 1554 t nil)) 1555 for type = (flymake-diagnostic-type diag) 1556 for backend = (flymake-diagnostic-backend diag) 1557 for bname = (or (ignore-errors (symbol-name backend)) 1558 "(anonymous function)") 1559 for data-vec = `[,(format "%s" line) 1560 ,(format "%s" col) 1561 ,(propertize (format "%s" 1562 (flymake--lookup-type-property 1563 type 'flymake-type-name type)) 1564 'face (flymake--lookup-type-property 1565 type 'mode-line-face 'flymake-error)) 1566 ,(propertize 1567 (if bname 1568 (replace-regexp-in-string "\\(.\\)[^-]+\\(-\\|$\\)" 1569 "\\1\\2" bname) 1570 "(anon)") 1571 'help-echo (format "From `%s' backend" backend)) 1572 (,(replace-regexp-in-string "\n.*" "" 1573 (flymake-diagnostic-text diag)) 1574 mouse-face highlight 1575 help-echo "mouse-2: visit this diagnostic" 1576 face nil 1577 action flymake-goto-diagnostic 1578 mouse-action flymake-goto-diagnostic)] 1579 when (and line col) collect 1580 (list (list :diagnostic diag 1581 :line line 1582 :severity (flymake--lookup-type-property 1583 type 1584 'severity (warning-numeric-level :error))) 1585 (if project-root 1586 (vconcat `[(,(file-name-nondirectory file) 1587 help-echo ,(file-relative-name file project-root) 1588 face nil 1589 mouse-face highlight 1590 action flymake-goto-diagnostic 1591 mouse-action flymake-goto-diagnostic )] 1592 data-vec) 1593 data-vec)))) 1594 1595 (defun flymake--diagnostics-buffer-entries () 1596 "Get tabulated list entries for current tabulated list buffer. 1597 Expects `flymake--diagnostics-buffer-entries' to be bound to a 1598 buffer." 1599 ;; Do nothing if 'flymake--diagnostics-buffer-source' has not yet 1600 ;; been set to a valid buffer. This could happen when this function 1601 ;; is called too early. For example 'global-display-line-numbers-mode' 1602 ;; calls us from its mode hook, when the diagnostic buffer has just 1603 ;; been created by 'flymake-show-diagnostics-buffer', but is not yet 1604 ;; set up properly (Bug#40529). 1605 (when (bufferp flymake--diagnostics-buffer-source) 1606 (with-current-buffer flymake--diagnostics-buffer-source 1607 (when flymake-mode 1608 (flymake--tabulated-entries-1 (flymake-diagnostics) nil))))) 1609 1610 (defvar flymake--diagnostics-base-tabulated-list-format 1611 `[("Line" 5 ,(lambda (l1 l2) 1612 (< (plist-get (car l1) :line) 1613 (plist-get (car l2) :line))) 1614 :right-align t) 1615 ("Col" 3 nil :right-align t) 1616 ("Type" 8 ,(lambda (l1 l2) 1617 (< (plist-get (car l1) :severity) 1618 (plist-get (car l2) :severity)))) 1619 ("Backend" 8 t) 1620 ("Message" 0 t)]) 1621 1622 (define-derived-mode flymake-diagnostics-buffer-mode tabulated-list-mode 1623 "Flymake diagnostics" 1624 "A mode for listing Flymake diagnostics." 1625 (setq tabulated-list-format flymake--diagnostics-base-tabulated-list-format) 1626 (setq tabulated-list-entries 1627 'flymake--diagnostics-buffer-entries) 1628 (tabulated-list-init-header)) 1629 1630 (defun flymake--diagnostics-buffer-name () 1631 (format "*Flymake diagnostics for `%s'*" (current-buffer))) 1632 1633 (define-obsolete-function-alias 'flymake-show-diagnostics-buffer 1634 'flymake-show-buffer-diagnostics "1.2.1") 1635 1636 (defun flymake-show-buffer-diagnostics () 1637 "Show a list of Flymake diagnostics for current buffer." 1638 (interactive) 1639 (let* ((name (flymake--diagnostics-buffer-name)) 1640 (source (current-buffer)) 1641 (target (or (get-buffer name) 1642 (with-current-buffer (get-buffer-create name) 1643 (flymake-diagnostics-buffer-mode) 1644 (current-buffer))))) 1645 (with-current-buffer target 1646 (setq flymake--diagnostics-buffer-source source) 1647 (display-buffer (current-buffer)) 1648 (revert-buffer)))) 1649 1650 1651 ;;; Per-project diagnostic listing 1652 ;;; 1653 1654 (defvar flymake-list-only-diagnostics nil 1655 "Diagnostics list meant for listing, not highlighting. 1656 This variable holds an alist ((FILE-NAME . DIAGS) ...) where 1657 FILE-NAME is a string holding an absolute file name and DIAGS is 1658 a list of diagnostic objects created with with 1659 `flymake-make-diagnostic'. These diagnostics are never annotated 1660 as overlays in actual buffers: they merely serve as temporary 1661 stand-ins for more accurate diagnostics that are produced once 1662 the file they refer to is visited and `flymake-mode' is turned on 1663 in the resulting buffer. 1664 1665 Flymake backends that somehow gain sporadic information about 1666 diagnostics in neighbouring files may freely modify this variable 1667 by adding or removing entries to for those files. If the 1668 information about those neighbouring files is acquired repeatedly 1669 and reliably, it may be more sensible to report them as 1670 \"foreign\" diagnostics instead. 1671 1672 Commands such as `flymake-show-project-diagnostics' will include 1673 some of this variable's contents the diagnostic listings.") 1674 1675 (defvar-local flymake--project-diagnostic-list-project nil) 1676 1677 (define-derived-mode flymake-project-diagnostics-mode tabulated-list-mode 1678 "Flymake diagnostics" 1679 "A mode for listing Flymake diagnostics." 1680 (setq tabulated-list-format 1681 (vconcat [("File" 25 t)] 1682 flymake--diagnostics-base-tabulated-list-format)) 1683 (setq tabulated-list-entries 1684 'flymake--project-diagnostics-entries) 1685 (tabulated-list-init-header)) 1686 1687 (cl-defun flymake--project-diagnostics (&optional (project (project-current))) 1688 "Get all known relevant diagnostics for PROJECT." 1689 (let* ((root (project-root project)) 1690 (visited-buffers (cl-remove-if-not #'buffer-file-name (project-buffers project))) 1691 buffer-annotated-diags 1692 relevant-foreign-diags 1693 list-only-diags 1694 annotated-diag-files) 1695 (setq buffer-annotated-diags 1696 (cl-loop for buf in visited-buffers 1697 for diags = (with-current-buffer buf 1698 (flymake-diagnostics)) 1699 when diags do 1700 (push (buffer-file-name buf) annotated-diag-files) 1701 append (cl-sort diags #'< :key #'flymake-diagnostic-beg))) 1702 (cl-loop 1703 for buf in visited-buffers 1704 do (with-current-buffer buf 1705 (when (and flymake-mode flymake--state) 1706 (maphash 1707 (lambda (_backend state) 1708 (maphash 1709 (lambda (foreign-file diags) 1710 (setq foreign-file (expand-file-name foreign-file)) 1711 ;; FIXME: This is not right if more than one visited 1712 ;; source targets the same foreign file. Don't 1713 ;; think we can get away without some kind of 1714 ;; `cl-remove-duplicates' here that utilizes 1715 ;; `flymake--equal-diagnostic-p'. 1716 (unless (member foreign-file annotated-diag-files) 1717 (push foreign-file annotated-diag-files) 1718 (setq relevant-foreign-diags 1719 (append relevant-foreign-diags 1720 diags)))) 1721 (flymake--state-foreign-diags state))) 1722 flymake--state)))) 1723 (setq list-only-diags 1724 (cl-loop for (file-name . diags) in flymake-list-only-diagnostics 1725 if (and (string-prefix-p (expand-file-name root) file-name) 1726 (not (member file-name annotated-diag-files))) 1727 append diags)) 1728 (append buffer-annotated-diags relevant-foreign-diags list-only-diags))) 1729 1730 (defun flymake--project-diagnostics-entries () 1731 (let ((p (project-current))) 1732 (flymake--tabulated-entries-1 (flymake--project-diagnostics p) 1733 (project-root p)))) 1734 1735 (defun flymake--project-diagnostics-buffer (root) 1736 (get-buffer-create (format "*Flymake diagnostics for `%s'*" root))) 1737 1738 (defun flymake-show-project-diagnostics () 1739 "Show a list of Flymake diagnostics for the current project." 1740 (interactive) 1741 (let* ((prj (project-current)) 1742 (root (project-root prj)) 1743 (buffer (flymake--project-diagnostics-buffer root))) 1744 (with-current-buffer buffer 1745 (flymake-project-diagnostics-mode) 1746 (setq-local flymake--project-diagnostic-list-project prj) 1747 (display-buffer (current-buffer)) 1748 (revert-buffer)))) 1749 1750 (defun flymake--update-diagnostics-listings (buffer) 1751 "Update diagnostics listings somehow relevant to BUFFER" 1752 (dolist (probe (buffer-list)) 1753 (with-current-buffer probe 1754 (when (or (and (eq major-mode 'flymake-project-diagnostics-mode) 1755 flymake--project-diagnostic-list-project 1756 (buffer-file-name buffer) 1757 (memq buffer 1758 (project-buffers flymake--project-diagnostic-list-project))) 1759 (and (eq major-mode 'flymake-diagnostics-buffer-mode) 1760 (eq flymake--diagnostics-buffer-source buffer))) 1761 (revert-buffer))))) 1762 1763 (provide 'flymake) 1764 1765 (require 'flymake-proc) 1766 1767 ;;; flymake.el ends here