summaryrefslogtreecommitdiff
path: root/lisp/emacs-lisp/cursor-sensor.el
diff options
context:
space:
mode:
Diffstat (limited to 'lisp/emacs-lisp/cursor-sensor.el')
-rw-r--r--lisp/emacs-lisp/cursor-sensor.el227
1 files changed, 227 insertions, 0 deletions
diff --git a/lisp/emacs-lisp/cursor-sensor.el b/lisp/emacs-lisp/cursor-sensor.el
new file mode 100644
index 00000000000..a3b40ef8b24
--- /dev/null
+++ b/lisp/emacs-lisp/cursor-sensor.el
@@ -0,0 +1,227 @@
+;;; cursor-sensor.el --- React to cursor movement -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2015-2022 Free Software Foundation, Inc.
+
+;; Author: Stefan Monnier <monnier@iro.umontreal.ca>
+;; Keywords:
+
+;; 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 <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This package implements the `cursor-intangible' and
+;; `cursor-sensor-functions' properties, which are meant to replace
+;; the old `intangible', `point-entered', and `point-left' properties.
+
+;; To use `cursor-intangible', just enable the
+;; `cursor-intangible-mode' minor mode, after which this package will
+;; move point away from any position that has a non-nil
+;; `cursor-intangible' property. This is only done just before
+;; redisplay happens, contrary to the old `intangible' property which
+;; was done at a much lower level.
+
+;; To use `cursor-sensor-functions', enable the `cursor-sensor-mode'
+;; minor mode, after which the `cursor-sensor-functions' will be
+;; called just before redisplay happens, according to the movement of
+;; the cursor since the last redisplay.
+
+;;;; Motivation
+
+;; The old properties were very problematic in practice because they
+;; operate at a much lower level and hence affect all motion
+;; *functions* like goto-char, forward-char, ... hence breaking
+;; invariants like:
+;;
+;; (forward-char N) == (progn (forward-char N1) (forward-char (- N N1)))
+;; (point) == (progn (forward-char N) (forward-char -N) (point))
+;; (+ N (point)) == (progn (forward-char N) (point))
+;;
+;; The problems would usually show up due to interaction between
+;; unrelated code working in the same buffer, where one code used those
+;; properties and the other (unknowingly) assumed those aren't used.
+;; In practice a *lot* of code assumes there's no such funny business.
+;;
+;; Worse: all(?) packages using those properties don't actually want those
+;; properties to affect motion at such a low-level, they only want to
+;; affect the overall effect of commands, but not the effect of every
+;; single point-motion that a given command happened to use internally.
+
+;;; Code:
+
+;;;###autoload
+(defvar cursor-sensor-inhibit nil
+ "When non-nil, suspend `cursor-sensor-mode' and `cursor-intangible-mode'.
+By convention, this is a list of symbols where each symbol stands for the
+\"cause\" of the suspension.")
+
+(defun cursor-sensor--intangible-p (pos)
+ (let ((p (get-pos-property pos 'cursor-intangible)))
+ (if p
+ (let (a b)
+ (if (and (setq a (get-char-property pos 'cursor-intangible))
+ (setq b (if (> pos (point-min))
+ (get-char-property (1- pos) 'cursor-intangible)))
+ (not (eq a b)))
+ ;; If we're right between two different intangible thingies,
+ ;; we can stop here. This is not quite consistent with the
+ ;; interpretation of "if it's sticky, then this boundary is
+ ;; itself intangible", but it's convenient (and it better matches
+ ;; the behavior of `intangible', making it easier to port code).
+ nil p))
+ p)))
+
+(defun cursor-sensor-tangible-pos (curpos window &optional second-chance)
+ (let ((newpos curpos))
+ (when (cursor-sensor--intangible-p newpos)
+ (let ((oldpos (window-parameter window 'cursor-intangible--last-point)))
+ (cond
+ ((or (and (integerp oldpos) (< oldpos newpos))
+ (eq newpos (point-min)))
+ (while
+ (when (< newpos (point-max))
+ (setq newpos
+ (if (get-char-property newpos 'cursor-intangible)
+ (next-single-char-property-change
+ newpos 'cursor-intangible nil (point-max))
+ (1+ newpos)))
+ (cursor-sensor--intangible-p newpos))))
+ (t ;; (>= oldpos newpos)
+ (while
+ (when (> newpos (point-min))
+ (setq newpos
+ (if (get-char-property (1- newpos) 'cursor-intangible)
+ (previous-single-char-property-change
+ newpos 'cursor-intangible nil (point-min))
+ (1- newpos)))
+ (cursor-sensor--intangible-p newpos)))))
+ (if (not (and (or (eq newpos (point-min)) (eq newpos (point-max)))
+ (cursor-sensor--intangible-p newpos)))
+ ;; All clear, we're good to go.
+ newpos
+ ;; We're still on an intangible position because we bumped
+ ;; into an intangible BOB/EOB: try to move in the other direction.
+ (if second-chance
+ ;; Actually, we tried already and that failed!
+ curpos
+ (cursor-sensor-tangible-pos newpos window 'second-chance)))))))
+
+(defun cursor-sensor-move-to-tangible (window)
+ (let* ((curpos (window-point window))
+ (newpos (cursor-sensor-tangible-pos curpos window)))
+ (when newpos (set-window-point window newpos))
+ (set-window-parameter window 'cursor-intangible--last-point
+ (or newpos curpos))))
+
+(defun cursor-sensor--move-to-tangible (window)
+ (unless cursor-sensor-inhibit
+ (cursor-sensor-move-to-tangible window)))
+
+;;;###autoload
+(define-minor-mode cursor-intangible-mode
+ "Keep cursor outside of any `cursor-intangible' text property."
+ :global nil
+ (if cursor-intangible-mode
+ (add-hook 'pre-redisplay-functions #'cursor-sensor--move-to-tangible
+ nil t)
+ (remove-hook 'pre-redisplay-functions #'cursor-sensor--move-to-tangible t)))
+
+;;; Detect cursor movement.
+
+(defun cursor-sensor--detect (&optional window)
+ (with-current-buffer (window-buffer window)
+ (unless cursor-sensor-inhibit
+ (let* ((point (window-point window))
+ ;; It's often desirable to make the
+ ;; cursor-sensor-functions property non-sticky on both
+ ;; ends, but that means get-pos-property might never
+ ;; see it.
+ (new (or (get-char-property point 'cursor-sensor-functions)
+ (unless (<= (point-min) point)
+ (get-char-property (1- point)
+ 'cursor-sensor-functions))))
+ (old (window-parameter window 'cursor-sensor--last-state))
+ (oldposmark (car old))
+ (oldpos (or (if oldposmark (marker-position oldposmark))
+ (point-min)))
+ (start (min oldpos point))
+ (end (max oldpos point)))
+ (unless (or (null old) (eq (marker-buffer oldposmark) (current-buffer)))
+ ;; `window' does not display the same buffer any more!
+ (setcdr old nil))
+ (if (or (and (null new) (null (cdr old)))
+ (and (eq new (cdr old))
+ (eq (next-single-char-property-change
+ start 'cursor-sensor-functions nil end)
+ end)))
+ ;; Clearly nothing to do.
+ nil
+ ;; Maybe something to do. Let's see exactly what needs to run.
+ (let* ((missing-p
+ (lambda (f)
+ "Non-nil if F is missing somewhere between START and END."
+ (let ((pos start)
+ (missing nil))
+ (while (< pos end)
+ (setq pos (next-single-char-property-change
+ pos 'cursor-sensor-functions
+ nil end))
+ (unless (memq f (get-char-property
+ pos 'cursor-sensor-functions))
+ (setq missing t)))
+ missing)))
+ (window (selected-window)))
+ (dolist (f (cdr old))
+ (unless (and (memq f new) (not (funcall missing-p f)))
+ (funcall f window oldpos 'left)))
+ (dolist (f new)
+ (unless (and (memq f (cdr old)) (not (funcall missing-p f)))
+ (funcall f window oldpos 'entered)))))
+
+ ;; Remember current state for next time.
+ ;; Re-read cursor-sensor-functions since the functions may have moved
+ ;; window-point!
+ (if old
+ (progn (move-marker (car old) point)
+ (setcdr old new))
+ (set-window-parameter window 'cursor-sensor--last-state
+ (cons (copy-marker point) new)))))))
+
+;;;###autoload
+(define-minor-mode cursor-sensor-mode
+ "Handle the `cursor-sensor-functions' text property.
+This property should hold a list of functions which react to the motion
+of the cursor. They're called with three arguments (WINDOW OLDPOS DIR)
+where WINDOW is the affected window, OLDPOS is the last known position of
+the cursor and DIR can be `entered' or `left' depending on whether the cursor
+is entering the area covered by the text-property property or leaving it."
+ :global nil
+ (cond
+ (cursor-sensor-mode
+ ;; Also add ourselves to `post-command-hook' because
+ ;; `pre-redisplay-functions' are sometimes called too late (after
+ ;; adjust_point_for_property has moved point, which makes it
+ ;; "impossible" for cursor-sensor-functions to do things like
+ ;; revealing invisible text).
+ (add-hook 'post-command-hook #'cursor-sensor--detect nil t)
+ (add-hook 'pre-redisplay-functions #'cursor-sensor--detect
+ nil t))
+ (t
+ (remove-hook 'post-command-hook #'cursor-sensor--detect t)
+ (remove-hook 'pre-redisplay-functions #'cursor-sensor--detect
+ t))))
+
+(provide 'cursor-sensor)
+;;; cursor-sensor.el ends here