From 3e7dff88928b568f8d4126c7fe2251662d140be6 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 15 Jun 2018 12:17:37 +0100 Subject: [PATCH] Flymake and backends exchange hints abouts changed regions * lisp/progmodes/flymake.el (flymake--delete-own-overlays): Accept BEG and END. Rename from flymake-delete-own-overlays. (flymake-diagnostic-functions): Describe :region, :recent-changes in docstring. (flymake--handle-report): Accept REGION. (flymake--run-backend): Accept optional ARGS to pass to backend fn. (flymake--recent-changes): New buffer-local variable. (flymake-start): Call flymake--run-backend with recent changes. (flymake-mode): Initialize flymake--recent-changes. Call flymake--delete-own-overlays. (flymake-after-change-function): Collect recent changes. * doc/misc/flymake.texi (Backend functions): Describe :recent-changes and :region. * etc/NEWS (Flymake): Mention improvements in backend communication. --- doc/misc/flymake.texi | 40 ++++++++++--- etc/NEWS | 4 ++ lisp/progmodes/flymake.el | 123 ++++++++++++++++++++++++++------------ 3 files changed, 120 insertions(+), 47 deletions(-) diff --git a/doc/misc/flymake.texi b/doc/misc/flymake.texi index 502d408f2b8..1e7a5e82c61 100644 --- a/doc/misc/flymake.texi +++ b/doc/misc/flymake.texi @@ -437,18 +437,35 @@ calling convention: one for calls made by Flymake into the backend via the backend function, the other in the reverse direction via a callback. To be usable, backends must adhere to both. -Backend functions must accept an arbitrary number of arguments: +The first argument passed to a backend function is always +@var{report-fn}, a callback function detailed below. Beyond it, +functions must be prepared to accept (and possibly ignore) an +arbitrary number of keyword-value pairs of the form +@w{@code{(@var{:key} @var{value} @var{:key2} @var{value2}...)}}. + +Currently, Flymake may pass the following keywords and values to the +backend function: @itemize -@item -the first argument is always @var{report-fn}, a callback function -detailed below; -@item -the remaining arguments are keyword-value pairs of the form -@w{@code{(@var{:key} @var{value} @var{:key2} @var{value2}...)}}. -Currently, Flymake provides no such arguments, but backend functions -must be prepared to accept (and possibly ignore) any number of them. +@item @code{:recent-changes} +The value is a list recent changes since the last time the backend +function was called for the buffer. If the list is empty, this +indicates that no changes have been recorded. If it is the first time +that this backend function is called for this activation of +@code{flymake-mode}, then this argument isn't provided at all +(i.e. it's not merely nil). + +Each element is in the form (@var{beg} @var{end} @var{text}) where +@var{beg} and @var{end} are buffer positions, and @var{text} is a +string containing the text contained between those positions (if any), +after the change was performed. + +@item @code{:changes-start} and @code{:changes-end} +The value is, repectively, the minimum and maximum buffer positions +touched by the recent changes. These are provided for convenience and +only if @code{:recent-changes} is also provided. + @end itemize Whenever Flymake or the user decide to re-check the buffer, backend @@ -504,6 +521,11 @@ details of the situation encountered, if any. @code{:force}, whose value should be a boolean suggesting that Flymake consider the report even if it was somehow unexpected. + +@item +@code{:region}, a cons (@var{beg} . @var{end}) of buffer positions +indicating that the report applies to that region and that previous +reports targeting other parts of the buffer remain valid. @end itemize @menu diff --git a/etc/NEWS b/etc/NEWS index 50433eb7f28..e89402db131 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -193,6 +193,10 @@ You should instead set properties on known diagnostic symbols, like *** New customizable variable 'flymake-start-on-save-buffer' Control whether Flymake starts checking the buffer on save. +*** Flymake and backend functions may exchange hints about buffer changes +This enables more efficient backends. See the docstring of +'flymake-diagnostic-functions' or the Flymake manual for details. + ** Package *** New 'package-quickstart' feature When 'package-quickstart' is non-nil, package.el precomputes a big autoloads diff --git a/lisp/progmodes/flymake.el b/lisp/progmodes/flymake.el index fdb22ccaf34..e3c07fc898c 100644 --- a/lisp/progmodes/flymake.el +++ b/lisp/progmodes/flymake.el @@ -354,9 +354,9 @@ verify FILTER, a function, and sort them by COMPARE (using KEY)." #'identity)) ovs)))) -(defun flymake-delete-own-overlays (&optional filter) - "Delete all Flymake overlays in BUFFER." - (mapc #'delete-overlay (flymake--overlays :filter filter))) +(defun flymake--delete-own-overlays (&optional filter beg end) + "Delete Flymake overlays matching FILTER between BEG and END." + (mapc #'delete-overlay (flymake--overlays :filter filter :beg beg :end end))) (defface flymake-error '((((supports :underline (:style wave))) @@ -444,9 +444,25 @@ number of arguments: detailed below; * the remaining arguments are keyword-value pairs in the - form (:KEY VALUE :KEY2 VALUE2...). Currently, Flymake provides - no such arguments, but backend functions must be prepared to - accept and possibly ignore any number of them. + form (:KEY VALUE :KEY2 VALUE2...). + +Currently, Flymake may provide these keyword-value pairs: + +* `:recent-changes', a list of recent changes since the last time + the backend function was called for the buffer. An empty list + indicates that no changes have been reocrded. If it is the + first time that this backend function is called for this + activation of `flymake-mode', then this argument isn't provided + at all (i.e. it's not merely nil). + + Each element is in the form (BEG END TEXT) where BEG and END + are buffer positions, and text is a string containing the text + contained between those positions (if any) after the change was + performed. + +* `:changes-start' and `:changes-end' the minimum and maximum + buffer positions touched by the recent changes. These are only + provided if `:recent-changes' is also provided. Whenever Flymake or the user decides to re-check the buffer, backend functions are called as detailed above and are expected @@ -491,7 +507,11 @@ Currently accepted REPORT-KEY arguments are: the situation encountered, if any. * `:force': value should be a boolean suggesting that Flymake - consider the report even if it was somehow unexpected.") + consider the report even if it was somehow unexpected. + +* `:region': a cons (BEG . END) of buffer positions indicating + that the report applies to that region and that previous + reports targeting other buffer regions are still valid.") (put 'flymake-diagnostic-functions 'safe-local-variable #'null) @@ -657,13 +677,15 @@ backend is operating normally.") (flymake-running-backends)) (cl-defun flymake--handle-report (backend token report-action - &key explanation force + &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 `flymake-diagnostic-functions' (which -see). Optional FORCE says to handle a report even if TOKEN was -not expected." +BACKEND, REPORT-ACTION and EXPLANATION, and FORCE conform to the +calling convention described in +`flymake-diagnostic-functions' (which see). Optional FORCE says +to handle a report even if TOKEN was not expected. 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 (not (flymake--backend-state-reported-p state)))) (setf (flymake--backend-state-reported-p state) t) @@ -695,13 +717,18 @@ not expected." (setq new-diags report-action) (save-restriction (widen) - ;; only delete overlays if this is the first report - (when first-report - (flymake-delete-own-overlays - (lambda (ov) - (eq backend - (flymake--diag-backend - (overlay-get ov 'flymake-diagnostic)))))) + ;; Decide whether to delete some of this backend's overlays + (let ((ov-filter + (lambda (ov) + (eq backend + (flymake--diag-backend + (overlay-get ov 'flymake-diagnostic)))))) + (cond + (region (flymake--delete-own-overlays ov-filter + (car region) + (cdr region))) + (first-report (flymake--delete-own-overlays ov-filter)))) + ;; Now make new ones (mapc (lambda (diag) (flymake--highlight-line diag) (setf (flymake--diag-backend diag) backend)) @@ -776,8 +803,10 @@ If it is running also stop it." (flymake--backend-state-disabled state) explanation (flymake--backend-state-reported-p state) t))) -(defun flymake--run-backend (backend) - "Run the backend BACKEND, reenabling if necessary." +(defun flymake--run-backend (backend &optional args) + "Run the backend BACKEND, re-enabling if necessary. +ARGS is a keyword-value plist passed to the backend along +with a report function." (flymake-log :debug "Running backend %s" backend) (let ((run-token (cl-gensym "backend-token"))) (flymake--with-backend-state backend state @@ -794,16 +823,19 @@ If it is running also stop it." ;; backend) will trigger an annoying backtrace. ;; (condition-case err - (funcall backend - (flymake-make-report-fn backend run-token)) + (apply backend (flymake-make-report-fn backend run-token) + args) (error (flymake--disable-backend backend err))))) +(defvar-local flymake--recent-changes nil + "Recent changes collected by `flymake-after-change-function'.") + (defun flymake-start (&optional deferred force) "Start a syntax check for the current buffer. DEFERRED is a list of symbols designating conditions to wait for before actually starting the check. If it is nil (the list is -empty), start it immediately, else defer the check to when those + empty), start it immediately, else defer the check to when those conditions are met. Currently recognized conditions are `post-command', for waiting until the current command is over, `on-display', for waiting until the buffer is actually displayed @@ -844,18 +876,30 @@ Interactively, with a prefix arg, FORCE is t." 'append 'local)) (t (setq flymake-check-start-time (float-time)) - (run-hook-wrapped - 'flymake-diagnostic-functions - (lambda (backend) - (cond - ((and (not force) - (flymake--with-backend-state backend state - (flymake--backend-state-disabled state))) - (flymake-log :debug "Backend %s is disabled, not starting" - backend)) - (t - (flymake--run-backend backend))) - nil))))))) + (let ((backend-args + (and + flymake--recent-changes + (list :recent-changes + flymake--recent-changes + :changes-start + (cl-reduce + #'min (mapcar #'car flymake--recent-changes)) + :changes-end + (cl-reduce + #'max (mapcar #'cadr flymake--recent-changes)))))) + (setq flymake--recent-changes nil) + (run-hook-wrapped + 'flymake-diagnostic-functions + (lambda (backend) + (cond + ((and (not force) + (flymake--with-backend-state backend state + (flymake--backend-state-disabled state))) + (flymake-log :debug "Backend %s is disabled, not starting" + backend)) + (t + (flymake--run-backend backend backend-args))) + nil)))))))) (defvar flymake-mode-map (let ((map (make-sparse-keymap))) map) @@ -908,6 +952,7 @@ special *Flymake log* buffer." :group 'flymake :lighter (add-hook 'kill-buffer-hook 'flymake-kill-buffer-hook nil t) (setq flymake--backend-state (make-hash-table)) + (setq flymake--recent-changes nil) (when flymake-start-on-flymake-mode (flymake-start t))) @@ -918,7 +963,7 @@ special *Flymake log* buffer." :group 'flymake :lighter (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) + (flymake--delete-own-overlays) (when flymake-timer (cancel-timer flymake-timer) @@ -960,8 +1005,10 @@ Do it only if `flymake-no-changes-timeout' is non-nil." (make-obsolete 'flymake-mode-off 'flymake-mode "26.1") (defun flymake-after-change-function (start stop _len) - "Start syntax check for current buffer if it isn't already running." + "Start syntax check for current buffer if it isn't already running. +START and STOP and LEN are as in `after-change-functions'." (let((new-text (buffer-substring start stop))) + (push (list start stop new-text) flymake--recent-changes) (when (and flymake-start-syntax-check-on-newline (equal new-text "\n")) (flymake-log :debug "starting syntax check as new-line has been seen") (flymake-start t)) -- 2.39.2