;;; flymake-ui.el --- A universal on-the-fly syntax checker -*- lexical-binding: t; -*- ;; Copyright (C) 2003-2017 Free Software Foundation, Inc. ;; Author: Pavel Kobyakov ;; Maintainer: Leo Liu ;; Version: 0.3 ;; Keywords: c languages tools ;; This file is part of GNU Emacs. ;; GNU Emacs is free software: you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; GNU Emacs is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with GNU Emacs. If not, see . ;;; Commentary: ;; ;; Flymake is a minor Emacs mode performing on-the-fly syntax checks.xo ;; ;; This file contains the UI for displaying and interacting with the ;; results of such checks, as well as entry points for backends to ;; hook on to. Backends are sources of diagnostic info. ;; ;;; Code: (eval-when-compile (require 'cl-lib)) (defgroup flymake nil "Universal on-the-fly syntax checker." :version "23.1" :link '(custom-manual "(flymake) Top") :group 'tools) (defcustom flymake-error-bitmap '(exclamation-mark error) "Bitmap (a symbol) used in the fringe for indicating errors. The value may also be a list of two elements where the second element specifies the face for the bitmap. For possible bitmap symbols, see `fringe-bitmaps'. See also `flymake-warning-bitmap'. The option `flymake-fringe-indicator-position' controls how and where this is used." :group 'flymake :version "24.3" :type '(choice (symbol :tag "Bitmap") (list :tag "Bitmap and face" (symbol :tag "Bitmap") (face :tag "Face")))) (defcustom flymake-warning-bitmap 'question-mark "Bitmap (a symbol) used in the fringe for indicating warnings. The value may also be a list of two elements where the second element specifies the face for the bitmap. For possible bitmap symbols, see `fringe-bitmaps'. See also `flymake-error-bitmap'. The option `flymake-fringe-indicator-position' controls how and where this is used." :group 'flymake :version "24.3" :type '(choice (symbol :tag "Bitmap") (list :tag "Bitmap and face" (symbol :tag "Bitmap") (face :tag "Face")))) (defcustom flymake-fringe-indicator-position 'left-fringe "The position to put flymake fringe indicator. The value can be nil (do not use indicators), `left-fringe' or `right-fringe'. See `flymake-error-bitmap' and `flymake-warning-bitmap'." :group 'flymake :version "24.3" :type '(choice (const left-fringe) (const right-fringe) (const :tag "No fringe indicators" nil))) (defcustom flymake-start-syntax-check-on-newline t "Start syntax check if newline char was added/removed from the buffer." :group 'flymake :type 'boolean) (defcustom flymake-no-changes-timeout 0.5 "Time to wait after last change before starting compilation." :group 'flymake :type 'number) (defcustom flymake-gui-warnings-enabled t "Enables/disables GUI warnings." :group 'flymake :type 'boolean) (make-obsolete-variable 'flymake-gui-warnings-enabled "it no longer has any effect." "26.1") (defcustom flymake-start-syntax-check-on-find-file t "Start syntax check on find file." :group 'flymake :type 'boolean) (defcustom flymake-log-level -1 "Logging level, only messages with level lower or equal will be logged. -1 = NONE, 0 = ERROR, 1 = WARNING, 2 = INFO, 3 = DEBUG" :group 'flymake :type 'integer) (defcustom flymake-backends '() "Ordered list of backends providing syntax check information for a buffer. Value is an alist of conses (PREDICATE . CHECKER). Both PREDICATE and CHECKER are functions called with a single argument, the buffer in which `flymake-mode' was enabled. PREDICATE is expected to (quickly) return t or nil if the buffer can be syntax checked by CHECKER, which in can performs more morose operations, possibly asynchronously." :group 'flymake :type 'alist) (defvar-local flymake-timer nil "Timer for starting syntax check.") (defvar-local flymake-last-change-time nil "Time of last buffer change.") (defvar-local flymake-check-start-time nil "Time at which syntax check was started.") (defvar-local flymake-check-was-interrupted nil "Non-nil if syntax check was killed by `flymake-compile'.") (defvar-local flymake-err-info nil "Sorted list of line numbers and lists of err info in the form (file, err-text).") (defvar-local flymake-new-err-info nil "Same as `flymake-err-info', effective when a syntax check is in progress.") (defun flymake-log (level text &rest args) "Log a message at level LEVEL. If LEVEL is higher than `flymake-log-level', the message is ignored. Otherwise, it is printed using `message'. TEXT is a format control string, and the remaining arguments ARGS are the string substitutions (see the function `format')." (if (<= level flymake-log-level) (let* ((msg (apply #'format-message text args))) (message "%s" msg)))) (defun flymake-ins-after (list pos val) "Insert VAL into LIST after position POS. POS counts from zero." (let ((tmp (copy-sequence list))) (setcdr (nthcdr pos tmp) (cons val (nthcdr (1+ pos) tmp))) tmp)) (defun flymake-set-at (list pos val) "Set VAL at position POS in LIST. POS counts from zero." (let ((tmp (copy-sequence list))) (setcar (nthcdr pos tmp) val) tmp)) (defun flymake-er-make-er (line-no line-err-info-list) (list line-no line-err-info-list)) (defun flymake-er-get-line (err-info) (nth 0 err-info)) (defun flymake-er-get-line-err-info-list (err-info) (nth 1 err-info)) (cl-defstruct (flymake-ler (:constructor nil) (:constructor flymake-ler-make-ler (file line type text &optional full-file))) file line type text full-file) (defun flymake-ler-set-file (line-err-info file) (flymake-ler-make-ler file (flymake-ler-line line-err-info) (flymake-ler-type line-err-info) (flymake-ler-text line-err-info) (flymake-ler-full-file line-err-info))) (defun flymake-ler-set-full-file (line-err-info full-file) (flymake-ler-make-ler (flymake-ler-file line-err-info) (flymake-ler-line line-err-info) (flymake-ler-type line-err-info) (flymake-ler-text line-err-info) full-file)) (defun flymake-ler-set-line (line-err-info line) (flymake-ler-make-ler (flymake-ler-file line-err-info) line (flymake-ler-type line-err-info) (flymake-ler-text line-err-info) (flymake-ler-full-file line-err-info))) (defun flymake-get-line-err-count (line-err-info-list type) "Return number of errors of specified TYPE. Value of TYPE is either \"e\" or \"w\"." (let* ((idx 0) (count (length line-err-info-list)) (err-count 0)) (while (< idx count) (when (equal type (flymake-ler-type (nth idx line-err-info-list))) (setq err-count (1+ err-count))) (setq idx (1+ idx))) err-count)) (defun flymake-get-err-count (err-info-list type) "Return number of errors of specified TYPE for ERR-INFO-LIST." (let* ((idx 0) (count (length err-info-list)) (err-count 0)) (while (< idx count) (setq err-count (+ err-count (flymake-get-line-err-count (nth 1 (nth idx err-info-list)) type))) (setq idx (1+ idx))) err-count)) (defun flymake-highlight-err-lines (err-info-list) "Highlight error lines in BUFFER using info from ERR-INFO-LIST." (save-excursion (dolist (err err-info-list) (flymake-highlight-line (car err) (nth 1 err))))) (defun flymake-overlay-p (ov) "Determine whether overlay OV was created by flymake." (and (overlayp ov) (overlay-get ov 'flymake-overlay))) (defun flymake-make-overlay (beg end tooltip-text face bitmap) "Allocate a flymake overlay in range BEG and END." (when (not (flymake-region-has-flymake-overlays beg end)) (let ((ov (make-overlay beg end nil t)) (fringe (and flymake-fringe-indicator-position (propertize "!" 'display (cons flymake-fringe-indicator-position (if (listp bitmap) bitmap (list bitmap))))))) (overlay-put ov 'face face) (overlay-put ov 'help-echo tooltip-text) (overlay-put ov 'flymake-overlay t) (overlay-put ov 'priority 100) (overlay-put ov 'evaporate t) (overlay-put ov 'before-string fringe) ;;+(flymake-log 3 "created overlay %s" ov) ov) (flymake-log 3 "created an overlay at (%d-%d)" beg end))) (defun flymake-delete-own-overlays () "Delete all flymake overlays in BUFFER." (dolist (ol (overlays-in (point-min) (point-max))) (when (flymake-overlay-p ol) (delete-overlay ol) ;;+(flymake-log 3 "deleted overlay %s" ol) ))) (defun flymake-region-has-flymake-overlays (beg end) "Check if region specified by BEG and END has overlay. Return t if it has at least one flymake overlay, nil if no overlay." (let ((ov (overlays-in beg end)) (has-flymake-overlays nil)) (while (consp ov) (when (flymake-overlay-p (car ov)) (setq has-flymake-overlays t)) (setq ov (cdr ov))) has-flymake-overlays)) (defface flymake-errline '((((supports :underline (:style wave))) :underline (:style wave :color "Red1")) (t :inherit error)) "Face used for marking error lines." :version "24.4" :group 'flymake) (defface flymake-warnline '((((supports :underline (:style wave))) :underline (:style wave :color "DarkOrange")) (t :inherit warning)) "Face used for marking warning lines." :version "24.4" :group 'flymake) (defun flymake-highlight-line (line-no line-err-info-list) "Highlight line LINE-NO in current buffer. Perhaps use text from LINE-ERR-INFO-LIST to enhance highlighting." (goto-char (point-min)) (forward-line (1- line-no)) (pcase-let* ((beg (progn (back-to-indentation) (point))) (end (progn (end-of-line) (skip-chars-backward " \t\f\t\n" beg) (if (eq (point) beg) (line-beginning-position 2) (point)))) (tooltip-text (mapconcat #'flymake-ler-text line-err-info-list "\n")) (`(,face ,bitmap) (if (> (flymake-get-line-err-count line-err-info-list "e") 0) (list 'flymake-errline flymake-error-bitmap) (list 'flymake-warnline flymake-warning-bitmap)))) (flymake-make-overlay beg end tooltip-text face bitmap))) (defun flymake-find-err-info (err-info-list line-no) "Find (line-err-info-list pos) for specified LINE-NO." (if err-info-list (let* ((line-err-info-list nil) (pos 0) (count (length err-info-list))) (while (and (< pos count) (< (car (nth pos err-info-list)) line-no)) (setq pos (1+ pos))) (when (and (< pos count) (equal (car (nth pos err-info-list)) line-no)) (setq line-err-info-list (flymake-er-get-line-err-info-list (nth pos err-info-list)))) (list line-err-info-list pos)) '(nil 0))) (defun flymake-line-err-info-is-less-or-equal (line-one line-two) (or (string< (flymake-ler-type line-one) (flymake-ler-type line-two)) (and (string= (flymake-ler-type line-one) (flymake-ler-type line-two)) (not (flymake-ler-file line-one)) (flymake-ler-file line-two)) (and (string= (flymake-ler-type line-one) (flymake-ler-type line-two)) (or (and (flymake-ler-file line-one) (flymake-ler-file line-two)) (and (not (flymake-ler-file line-one)) (not (flymake-ler-file line-two))))))) (defun flymake-add-line-err-info (line-err-info-list line-err-info) "Update LINE-ERR-INFO-LIST with the error LINE-ERR-INFO. For the format of LINE-ERR-INFO, see `flymake-ler-make-ler'. The new element is inserted in the proper position, according to the predicate `flymake-line-err-info-is-less-or-equal'. The updated value of LINE-ERR-INFO-LIST is returned." (if (not line-err-info-list) (list line-err-info) (let* ((count (length line-err-info-list)) (idx 0)) (while (and (< idx count) (flymake-line-err-info-is-less-or-equal (nth idx line-err-info-list) line-err-info)) (setq idx (1+ idx))) (cond ((equal 0 idx) (setq line-err-info-list (cons line-err-info line-err-info-list))) (t (setq line-err-info-list (flymake-ins-after line-err-info-list (1- idx) line-err-info)))) line-err-info-list))) (defun flymake-add-err-info (err-info-list line-err-info) "Update ERR-INFO-LIST with the error LINE-ERR-INFO, preserving sort order. Returns the updated value of ERR-INFO-LIST. For the format of ERR-INFO-LIST, see `flymake-err-info'. For the format of LINE-ERR-INFO, see `flymake-ler-make-ler'." (let* ((line-no (if (flymake-ler-file line-err-info) 1 (flymake-ler-line line-err-info))) (info-and-pos (flymake-find-err-info err-info-list line-no)) (exists (car info-and-pos)) (pos (nth 1 info-and-pos)) (line-err-info-list nil) (err-info nil)) (if exists (setq line-err-info-list (flymake-er-get-line-err-info-list (car (nthcdr pos err-info-list))))) (setq line-err-info-list (flymake-add-line-err-info line-err-info-list line-err-info)) (setq err-info (flymake-er-make-er line-no line-err-info-list)) (cond (exists (setq err-info-list (flymake-set-at err-info-list pos err-info))) ((equal 0 pos) (setq err-info-list (cons err-info err-info-list))) (t (setq err-info-list (flymake-ins-after err-info-list (1- pos) err-info)))) err-info-list)) (defvar-local flymake-is-running nil "If t, flymake syntax check process is running for the current buffer.") (defun flymake-on-timer-event (buffer) "Start a syntax check for buffer BUFFER if necessary." (when (buffer-live-p buffer) (with-current-buffer buffer (when (and (not flymake-is-running) flymake-last-change-time (> (- (float-time) flymake-last-change-time) flymake-no-changes-timeout)) (setq flymake-last-change-time nil) (flymake-log 3 "starting syntax check as more than 1 second passed since last change") (flymake--start-syntax-check))))) (define-obsolete-function-alias 'flymake-display-err-menu-for-current-line 'flymake-popup-current-error-menu "24.4") (defun flymake-popup-current-error-menu (&optional event) "Pop up a menu with errors/warnings for current line." (interactive (list last-nonmenu-event)) (let* ((line-no (line-number-at-pos)) (errors (or (car (flymake-find-err-info flymake-err-info line-no)) (user-error "No errors for current line"))) (menu (mapcar (lambda (x) (if (flymake-ler-file x) (cons (format "%s - %s(%d)" (flymake-ler-text x) (flymake-ler-file x) (flymake-ler-line x)) x) (list (flymake-ler-text x)))) errors)) (event (if (mouse-event-p event) event (list 'mouse-1 (posn-at-point)))) (title (format "Line %d: %d error(s), %d warning(s)" line-no (flymake-get-line-err-count errors "e") (flymake-get-line-err-count errors "w"))) (choice (x-popup-menu event (list title (cons "" menu))))) (flymake-log 3 "choice=%s" choice) (when choice (flymake-goto-file-and-line (flymake-ler-full-file choice) (flymake-ler-line choice))))) (defun flymake-goto-file-and-line (file line) "Try to get buffer for FILE and goto line LINE in it." (if (not (file-exists-p file)) (flymake-log 1 "File %s does not exist" file) (find-file file) (goto-char (point-min)) (forward-line (1- line)))) ;; flymake minor mode declarations (defvar-local flymake-mode-line nil) (defvar-local flymake-mode-line-e-w nil) (defvar-local flymake-mode-line-status nil) (defun flymake-report-status (e-w &optional status) "Show status in mode line." (when e-w (setq flymake-mode-line-e-w e-w)) (when status (setq flymake-mode-line-status status)) (let* ((mode-line " Flymake")) (when (> (length flymake-mode-line-e-w) 0) (setq mode-line (concat mode-line ":" flymake-mode-line-e-w))) (setq mode-line (concat mode-line flymake-mode-line-status)) (setq flymake-mode-line mode-line) (force-mode-line-update))) ;; 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") (defun flymake-report-fatal-status (status warning) "Display a warning and switch flymake mode off." ;; This first message was always shown by default, and flymake-log ;; does nothing by default, hence the use of message. ;; Another option is display-warning. (if (< flymake-log-level 0) (message "Flymake: %s. Flymake will be switched OFF" warning)) (flymake-mode 0) (flymake-log 0 "switched OFF Flymake mode for buffer %s due to fatal status %s, warning %s" (buffer-name) status warning)) (defvar-local flymake--backend nil "The currently active backend selected by `flymake-mode'") (defun flymake--can-syntax-check-buffer (buffer) (let ((all flymake-backends) (candidate)) (catch 'done (while (setq candidate (pop all)) (when (with-current-buffer buffer (funcall (car candidate))) (throw 'done (cdr candidate))))))) (defun flymake--start-syntax-check () (funcall flymake--backend)) ;;;###autoload (define-minor-mode flymake-mode nil :group 'flymake :lighter flymake-mode-line (cond ;; Turning the mode ON. (flymake-mode (let* ((backend (flymake--can-syntax-check-buffer (current-buffer)))) (cond ((not backend) (flymake-log 2 "flymake cannot check syntax in buffer %s" (buffer-name))) (t (setq flymake--backend backend) (add-hook 'after-change-functions 'flymake-after-change-function nil t) (add-hook 'after-save-hook 'flymake-after-save-hook nil t) (add-hook 'kill-buffer-hook 'flymake-kill-buffer-hook nil t) ;;+(add-hook 'find-file-hook 'flymake-find-file-hook) (flymake-report-status "" "") (setq flymake-timer (run-at-time nil 1 'flymake-on-timer-event (current-buffer))) (when (and flymake-start-syntax-check-on-find-file ;; Since we write temp files in current dir, there's no point ;; trying if the directory is read-only (bug#8954). (file-writable-p (file-name-directory buffer-file-name))) (with-demoted-errors (flymake--start-syntax-check))))) ) ) ;; Turning the mode OFF. (t (setq flymake--backend nil) (remove-hook 'after-change-functions 'flymake-after-change-function t) (remove-hook 'after-save-hook 'flymake-after-save-hook t) (remove-hook 'kill-buffer-hook 'flymake-kill-buffer-hook t) ;;+(remove-hook 'find-file-hook (function flymake-find-file-hook) t) (flymake-delete-own-overlays) (when flymake-timer (cancel-timer flymake-timer) (setq flymake-timer nil)) (setq flymake-is-running nil)))) ;; disabling flymake-mode is safe, enabling - not necessarily so (put 'flymake-mode 'safe-local-variable 'null) ;;;###autoload (defun flymake-mode-on () "Turn flymake mode on." (flymake-mode 1) (flymake-log 1 "flymake mode turned ON for buffer %s" (buffer-name))) ;;;###autoload (defun flymake-mode-off () "Turn flymake mode off." (flymake-mode 0) (flymake-log 1 "flymake mode turned OFF for buffer %s" (buffer-name))) (defun flymake-after-change-function (start stop _len) "Start syntax check for current buffer if it isn't already running." ;;+(flymake-log 0 "setting change time to %s" (float-time)) (let((new-text (buffer-substring start stop))) (when (and flymake-start-syntax-check-on-newline (equal new-text "\n")) (flymake-log 3 "starting syntax check as new-line has been seen") (flymake--start-syntax-check)) (setq flymake-last-change-time (float-time)))) (defun flymake-after-save-hook () (if (local-variable-p 'flymake-mode (current-buffer)) ; (???) other way to determine whether flymake is active in buffer being saved? (progn (flymake-log 3 "starting syntax check as buffer was saved") (flymake--start-syntax-check)))) ; no more mode 3. cannot start check if mode 3 (to temp copies) is active - (???) (defun flymake-kill-buffer-hook () (when flymake-timer (cancel-timer flymake-timer) (setq flymake-timer nil))) ;;;###autoload (defun flymake-find-file-hook () ;;+(when flymake-start-syntax-check-on-find-file ;;+ (flymake-log 3 "starting syntax check on file open") ;;+ (flymake--start-syntax-check) ;;+) (when (and (not (local-variable-p 'flymake-mode (current-buffer))) (flymake--can-syntax-check-buffer (current-buffer))) (flymake-mode) (flymake-log 3 "automatically turned ON flymake mode"))) (defun flymake-get-first-err-line-no (err-info-list) "Return first line with error." (when err-info-list (flymake-er-get-line (car err-info-list)))) (defun flymake-get-last-err-line-no (err-info-list) "Return last line with error." (when err-info-list (flymake-er-get-line (nth (1- (length err-info-list)) err-info-list)))) (defun flymake-get-next-err-line-no (err-info-list line-no) "Return next line with error." (when err-info-list (let* ((count (length err-info-list)) (idx 0)) (while (and (< idx count) (>= line-no (flymake-er-get-line (nth idx err-info-list)))) (setq idx (1+ idx))) (if (< idx count) (flymake-er-get-line (nth idx err-info-list)))))) (defun flymake-get-prev-err-line-no (err-info-list line-no) "Return previous line with error." (when err-info-list (let* ((count (length err-info-list))) (while (and (> count 0) (<= line-no (flymake-er-get-line (nth (1- count) err-info-list)))) (setq count (1- count))) (if (> count 0) (flymake-er-get-line (nth (1- count) err-info-list)))))) (defun flymake-skip-whitespace () "Move forward until non-whitespace is reached." (while (looking-at "[ \t]") (forward-char))) (defun flymake-goto-line (line-no) "Go to line LINE-NO, then skip whitespace." (goto-char (point-min)) (forward-line (1- line-no)) (flymake-skip-whitespace)) (defun flymake-goto-next-error () "Go to next error in err ring." (interactive) (let ((line-no (flymake-get-next-err-line-no flymake-err-info (line-number-at-pos)))) (when (not line-no) (setq line-no (flymake-get-first-err-line-no flymake-err-info)) (flymake-log 1 "passed end of file")) (if line-no (flymake-goto-line line-no) (flymake-log 1 "no errors in current buffer")))) (defun flymake-goto-prev-error () "Go to previous error in err ring." (interactive) (let ((line-no (flymake-get-prev-err-line-no flymake-err-info (line-number-at-pos)))) (when (not line-no) (setq line-no (flymake-get-last-err-line-no flymake-err-info)) (flymake-log 1 "passed beginning of file")) (if line-no (flymake-goto-line line-no) (flymake-log 1 "no errors in current buffer")))) (defun flymake-patch-err-text (string) (if (string-match "^[\n\t :0-9]*\\(.*\\)$" string) (match-string 1 string) string)) (provide 'flymake-ui) ;;; flymake-ui.el ends here