Khalid Rafi <[email protected]> writes:

> Changing subject of this thread for adding it to
> https://tracker.orgmode.org.
>
> Thanks for contributing to Org mode.

Thanks.

TLDR: org-inside makes it easy to precisely edit text with hidden
components like emphasis markers by changing appearance (cursor type,
face) and/or unhiding when point is "inside" such entities.

For people who prefer a patch, see below for the most recent version.
Testing welcome.

Follow the latest on the feature/org-inside branch:

  
https://cgit.git.savannah.nongnu.org/cgit/org-mode.git/log/?h=feature/org-inside

diff --git a/doc/org-manual.org b/doc/org-manual.org
index baa1127a0..10a7c72a9 100644
--- a/doc/org-manual.org
+++ b/doc/org-manual.org
@@ -11407,6 +11407,8 @@ stops emphasis there./
 You can use zero width space to help Org sorting out the ambiguity.
 See [[*Escape Character]] for more details.
 
+For help editing text with hidden emphasis marks, see [[*Dealing with Hidden Text]].
+
 ** Subscripts and Superscripts
 :PROPERTIES:
 :DESCRIPTION: Simple syntax for raising/lowering text.
@@ -11456,6 +11458,9 @@ Set both ~org-pretty-entities~ and
 super- and subscripts /visually/ interpreted as specified by the
 option ~org-use-sub-superscripts~.
 
+For help editing text with hidden sub- and superscript markers, see
+[[*Dealing with Hidden Text]].
+
 ** Special Symbols
 :PROPERTIES:
 :DESCRIPTION: Greek letters and other symbols.
@@ -20929,6 +20934,7 @@ option ~org-startup-numerated~ to =t=, or locally on a file by using
 ** The Very Busy {{{kbd(C-c C-c)}}} Key
 :PROPERTIES:
 :DESCRIPTION: When in doubt, press @kbd{C-c C-c}.
+:ID:       ACEBFD46-09A6-43E5-A868-A4B3CF1F56AF
 :END:
 #+kindex: C-c C-c
 #+cindex: @kbd{C-c C-c}, overview
@@ -20979,6 +20985,9 @@ manual, but here is a consolidated list for easy reference.
 
 - If point is at a timestamp, fix the day name in the timestamp.
 
+- If ~org-inside~ is enabled (see [[*Dealing with Hidden Text]]) and
+  point is inside text with hidden contents, toggle hiding.
+
 ** Summary of In-Buffer Settings
 :PROPERTIES:
 :DESCRIPTION: Overview of keywords.
@@ -21139,11 +21148,12 @@ changes.
 
   #+vindex: org-link-descriptive
   Bracket links in Org buffers are displayed hiding the link path and
-  brackets.  For example, =[[https://orgmode.org][Org Website]]= is,
-  by default, displayed as "Org Website", hiding the link itself and
-  just displaying its description.  Alternatively, the links can be
+  brackets.  For example, =[[https://orgmode.org][Org Website]]= is, by
+  default, displayed as "Org Website", hiding the link itself and just
+  displaying its description.  Alternatively, the links can be
   displayed in full.  The corresponding variable is
-  ~org-link-descriptive~.
+  ~org-link-descriptive~.  For help editing text with hidden links and
+  brackets, see [[*Dealing with Hidden Text]].
 
   | =descriptivelinks= | Hide path and brackets in links. |
   | =literallinks=     | Do not hide anything.            |
@@ -21359,6 +21369,33 @@ may write instead
 
 where =X= denotes the zero width space character.
 
+** Dealing with Hidden Text
+:PROPERTIES:
+:DESCRIPTION: Using ~org-inside~ to help with editing text with hidden contents.
+:END:
+#+kindex: C-c C-c
+#+cindex: org-inside, revealing hidden components, hidden text components
+
+In Org, entities including emphasized text, links, and super/subscript
+can contain hidden components (markers, brackets, link URL, etc.).
+When the cursor is adjacent to such hidden text, point can be
+ambiguous: at the same apparent cursor position, it can be either
+/inside/ or /outside/ the hidden region.  This can make it difficult to
+edit precisely.
+
+To alleviate this problem, activate the mode ~org-inside~.  When this
+minor mode is active, the appearance is changed when point is "inside"
+such an entity, to make it clear where you are.  Appearance changes
+can include face (e.g. a subtle background color), cursor type
+(e.g. bar instead of box), and unhiding.
+
+Hidden text can be unhidden on demand using {{{kbd(C-c C-c)}}} key
+(see [[*The Very Busy {{{kbd(C-c C-c)}}} Key]]) on an entity with
+hidden contents, and/or automatically upon entry, with an optional
+delay.  ~org-inside~ supports nested entities, applying face appearance
+changes to the innermost element.
+
+Note that certain features require Emacs v31 or later.
 ** Code Evaluation and Security Issues
 :PROPERTIES:
 :DESCRIPTION: Org files evaluate in-line code.
diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS
index c7e970aa0..6db077721 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -106,6 +106,17 @@ This tangles the block to four files:
 - /backup/config.el
 - /backup/backup.el
 
+*** New mode ~org-inside~, to make editing entities with hidden elements easier
+
+The new ~org-inside-mode~ makes it easier to edit entities with hidden
+contents like emphasized text, links, and super/subscripts.  When
+active, this mode alters the appearance when point is "inside"
+elements with hidden contents, so you know where edits will occur.
+The appearance changes are configurable, and can include face, cursor
+type, and unhiding, either automatic after entry (with a configurable
+delay), or toggled on-demand (via =C-c C-c=).  Nested elements with
+hidden contents are supported.
+
 ** New and changed options
 
 # Changes dealing with changing default values of customizations,
diff --git a/lisp/org-inside.el b/lisp/org-inside.el
new file mode 100644
index 000000000..2554f2606
--- /dev/null
+++ b/lisp/org-inside.el
@@ -0,0 +1,573 @@
+;;; org-inside.el --- Change appearance inside hidden contents entities  -*- lexical-binding: t; -*-
+;;
+;; Copyright (C) 2026 Free Software Foundation, Inc.
+;;
+;; Author:  J.D. Smith <jdtsmith at gmail dot com>
+;; Maintainer: J.D. Smith <jdtsmith at gmail dot com>
+;; Keywords: folding, invisible text
+;; URL: https://orgmode.org
+;;
+;; 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:
+
+;; In org, entities including emphasized text, links, and
+;; super/subscript can contain hidden components (markers, brackets,
+;; link URL, etc.).  When the cursor is adjacent to such hidden text,
+;; the point can be ambiguous: at the same apparent cursor position,
+;; point can be either inside or outside the hidden region.  This can
+;; make it difficult to edit precisely.
+;;
+;; To alleviate this problem, `org-inside' changes the appearance when
+;; point is "inside" such an entity, to make it clear where you are.
+;;
+;; Appearance changes are highly configurable, and can include
+;; changing the cursor type, text face (e.g. adding a colorful
+;; underline), and/or automatically unhiding the hidden text.  A
+;; command to hide/unhide the hidden text on demand is also provided
+;; and added to the context-dependent ctrl-c ctrl-c hook; see
+;; `org-inside-toggle-hidden'.
+;;
+;; This mode is intended to be used with setting that enable hidden
+;; components, e.g.  `org-hide-emphasis-markers', and/or
+;; `org-highlight-links' (with `bracket' / `org-descriptive-links') to
+;; make editing links and emphasized text easier.  If your version of
+;; Emacs supports it (v31+), nested entities with hidden contents are
+;; supported, with face changes applied to the innermost nested
+;; entity.
+
+;;;; For Developers:
+;;
+;; Note the shorthand substitution for window/overlay state structure
+;; slots; see file-local-variables:
+;;
+;;    ois/  => org-inside-state-
+;;
+;; On overlay-based invisibility: to hide/unhide components, we make
+;; use of the ability of overlays to override underlying text
+;; properties.  Attaching an invisible=nil overlay property allows the
+;; text properties to shine through like normal, as org intends
+;; (i.e. the invisible components do not appear), while a non-nil
+;; overlay 'invisible property, as long as it is _not_ on the
+;; `buffer-invisibility-spec' list, serves to _reveal_ the hidden
+;; components.
+
+;;; Code:
+(require 'org)
+(require 'org-element)
+(require 'face-remap)
+(require 'cus-start) ; ensure 'cursor-type has its 'custom-type set
+(require 'cl-seq)
+(require 'cl-extra)
+
+(defcustom org-inside-appearance '(:cursor bar :face org-inside-face)
+  "Special appearance when point is inside text with hidden contents.
+Appearance changes can include cursor type, text face, and unhiding.
+
+The value is a plist, with possible keys and values:
+
+ `:cursor': one of the possible `cursor-type' types to change to while
+     inside.
+
+ `:face': an optional face (or anonymous list of face attributes) to
+     apply to the visible text of the innermost entity.
+
+ `:unhide': a boolean indicating whether to automatically un-hide the
+     hidden contents within the outermost entity at point.  Unhiding can
+     also be toggled by command; see `org-inside-toggle-hidden'.  The
+     delay prior to automatically unhiding can be configured with
+     `org-inside-unhide-delay'.
+
+All appearance keys are optional, and can be freely combined.
+
+If `org-inside-appearance' is nil, no appearance changes will be applied
+when point is inside hidden contents entities.  The command
+`org-inside-toggle-hidden' can still be used to unhide hidden text.
+
+For nested entities with hidden contents, all appearance changes except
+`:face' apply to the outermost entity.  `:face' is applied to the
+innermost, when inside it (v31+ only)."
+  :group 'org-appearance
+  :type `(choice
+          (const :value nil :tag "No appearance changes: unhide on command only")
+          (plist
+           :tag "Specific appearance options"
+	   :options
+	   ((:cursor ,(get 'cursor-type 'custom-type)
+		     :tag "Cursor Type")
+	    (:face (choice (face :tag "Face Name")
+			   (plist :tag "Attribute List"))
+		   :tag "Text Face")
+	    (:unhide boolean :tag "Unhide hidden markers"))))
+  :set (lambda (sym val)
+         (set-default-toplevel-value sym val)
+         (when (featurep 'org-inside) (org-inside--reset-all))))
+
+(defcustom org-inside-unhide-delay 1.0
+  "Default delay in seconds before auto-unhiding entities.
+Requires the `:unhide' setting to be non-nil in `org-inside-appearance'.
+Set to zero or nil for instant auto-unhiding."
+  :group 'org-appearance
+  :type 'float)
+
+(defface org-inside-face
+  '((((class color) (background light)) (:background "grey90"))
+    (((class color) (background dark)) (:background "grey20")))
+  "Face used when inside hidden-contents entities."
+  :group 'org-faces)
+
+(defvar-local org-inside--hidden-contents-types nil
+  "Entity types with hidden contents.")
+
+(defun org-inside--setup-hidden-contents-types ()
+  "Setup entity types with hidden contents."
+  (setq org-inside--hidden-contents-types
+        `(,@(and org-hide-emphasis-markers
+                 '(bold code italic verbatim underline strike-through))
+          ,@(and org-link-descriptive '(link))
+          ,@(and org-pretty-entities
+                 org-pretty-entities-include-sub-superscripts
+                 '(subscript superscript)))))
+
+(defun org-inside--elems-at-point ()
+  "Return all org-entity with possible hidden contents at point.
+The innermost elements are first on the list."
+  (when-let* ((ctx (org-element-context)))
+    (org-element-lineage-map ctx #'identity
+      org-inside--hidden-contents-types t)))
+
+(defun org-inside--hidden-element ()
+  "Return non-nil if inside an element with hidden contents at point."
+  (cl-some
+   (lambda (el)
+     (get-text-property (org-element-begin el) 'invisible))
+   (org-inside--elems-at-point)))
+
+(defun org-inside--overlay-modification (_ov after-p &rest _r)
+  "Detect modifications of the entity covered by `org-inside' overlay OV.
+AFTER-P is t if called after the modification occurs.  If the org
+entit(ies) covered by OV no longer exists at point, we set the
+appearance for outside."
+  (when (and after-p (not (org-inside--hidden-element)))
+    (org-inside--set-appearance)))
+
+(defvar-local org-inside--states nil)
+;; Note shorthand: ois/ => org-inside-state-
+(cl-defstruct (org-inside-state
+               (:copier nil)
+               (:conc-name ois/)
+               (:constructor nil)
+               (:constructor ois/create))
+  "State mapping from windows to `org-inside' overlays."
+  ( ov nil :type overlay
+    :documentation "PRIMARY overlay.")
+  ( ov2 nil :type overlay
+    :documentation "SECONDARY overlay (face only, if needed).")
+  ( saved-cursor-type nil :type symbol
+    :documentation "The saved cursor type in WIN."))
+
+(defun ois/window (state)
+  "Return the window associated with STATE's primary overlay."
+  (when-let* ((ov (ois/ov state))
+              (_ (overlayp ov)))
+    (overlay-get ov 'window)))
+
+(defun org-inside--restore-cursor (win old-type)
+  "Restore old cursor in WIN to OLD-TYPE (if any).
+If OLD-TYPE is nil, use the `pending-cursor-type' parameter of WIN, if
+any.  If the current window cursor type is nil (i.e. the cursor is
+hidden), no change is made."
+  (let* ((pending-type (window-parameter win 'pending-cursor-type))
+         (old-type (or old-type pending-type)))
+    (when (and old-type win (window-live-p win) (window-cursor-type win))
+      (set-window-cursor-type win old-type)
+      (when pending-type
+        (set-window-parameter win 'pending-cursor-type nil)))))
+
+(defun org-inside--reset-state (state)
+  "Delete overlays and restore cursor in the window indicated by STATE."
+  (pcase-let (((cl-struct org-inside-state ov ov2 saved-cursor-type) state)
+              (window (ois/window state)))
+    (when (and window (window-live-p window))
+      (org-inside--restore-cursor window saved-cursor-type))
+    (when (overlayp ov) (delete-overlay ov))
+    (when (overlayp ov2) (delete-overlay ov2))))
+
+(defun org-inside--remove-state (state)
+  "Reset and remove STATE from the list of states in the current buffer."
+  (when org-inside--states
+    (org-inside--reset-state state)
+    (setq org-inside--states (cl-delete state org-inside--states))))
+
+(defvar org-inside-mode)
+(defun org-inside--trim-states (&optional all)
+  "Remove and reset states that no long apply to the current buffer.
+If ALL is non-nil, remove all states recorded for this buffer.
+Otherwise, remove only stale states for non-live windows, or windows
+showing other buffers.  Should be called from an `org-inside' buffer."
+  (setq org-inside--states
+	(cl-loop with buf = (current-buffer)
+		 for s in org-inside--states
+		 for win = (ois/window s)
+  		 if (and win (window-live-p win) (not all)
+                         (eq (window-buffer win) buf)
+                         org-inside-mode)
+		 collect s else do (org-inside--reset-state s))))
+
+(defun org-inside--make-overlay (win &optional unhide secondary-p)
+  "Return a new front-advancing overlay specific to window WIN.
+If SECONDARY-P is non-nil, create a secondary overlay without
+properties.  Otherwise, assign `cursor-sensor-functions' or
+`modification-hooks', and, if UNHIDE is non-nil, the `invisible'
+property."
+  (let* ((buf (window-buffer win))
+         (ov (make-overlay 1 1 buf t)))
+    (overlay-put ov 'window win)
+    (unless secondary-p
+      (overlay-put ov 'cursor-sensor-functions '(org-inside--sensor))
+      (overlay-put ov 'modification-hooks '(org-inside--overlay-modification))
+      ;; For auto-unhiding, we set the invisible property to something
+      ;; guaranteed not to be on the `buffer-invisibility-spec'.
+      (when unhide (overlay-put ov 'invisible 'org-inside--not-hidden)))
+    ov))
+
+(defun org-inside--state-for-window (win &optional unhide secondary-p)
+  "Return the state for window WIN, creating and caching it if necessary.
+UNHIDE is the hide status; see the custom variable
+`org-inside-appearance'.  If UNHIDE is non-nil, invisibility will be
+configured on the primary overlay to unhide the underlying hidden text.
+If SECONDARY-P is non-nil, a valid secondary overlay will be included in
+the returned state.  Otherwise, the secondary may be nil or invalid.
+Note that this function does not set the `face' property."
+  (let ((state (cl-find win org-inside--states :key #'ois/window)))
+    (if state             ; a saved state may lack a secondary overlay
+        (when (and secondary-p (not (overlayp (ois/ov2 state))))
+          (setf (ois/ov2 state) (org-inside--make-overlay win nil 'secondary)))
+      (setq state (ois/create
+                   :ov (org-inside--make-overlay win unhide)
+                   :ov2 (and secondary-p
+                             (org-inside--make-overlay win nil 'secondary))))
+      (cl-callf2 push state org-inside--states))
+    state))
+
+(defun org-inside--state (win unhide face &optional with-secondary-p)
+  "Return an appropriate `org-inside' state for window WIN.
+UNHIDE and FACE are the invisibility status and text face; see the
+custom variable `org-inside-appearance'.  Returns a structure of type
+`org-inside-state'.
+
+If FACE and WITH-SECONDARY-P are non-nil, ensure the SECONDARY overlay
+is valid and contains the `face' property set to FACE (and the PRIMARY
+contains no `face' property).  If WITH-SECONDARY-P is nil, the SECONDARY
+overlay may be invalid."
+  (let* ((sec-p (and face with-secondary-p (>= emacs-major-version 31)))
+         (state (org-inside--state-for-window win unhide sec-p)))
+    (overlay-put (ois/ov state) 'face (unless sec-p face)) ; always update face
+    (when sec-p (overlay-put (ois/ov2 state) 'face face))
+    state))
+
+(defvar-local org-inside--unhide-timer nil)
+
+(defun org-inside--set-appearance (&optional win beg end beg2 end2)
+  "Set appearance and hidden state for hidden-contents entities.
+The region is from BEG to END in window WIN's buffer.  If BEG or END are
+nil, point is considered to be outside the text and the prior
+\"outside\" appearance is restored.  If WIN is nil, the selected window
+is used.
+
+If BEG2 and END2 are non-nil, they represent a sub-range fully within
+BEG and END containing point, over which to apply face modifications
+using a secondary overlay.  This allows face modification to cover a
+smaller region within the main BEG..END range, for indicating nested
+entities.
+
+Note that if the `cursor-type' is configured to change inside but the
+`window-cursor-type' is currently nil (i.e. the cursor is hidden), the
+cursor is left hidden, and the window parameter `pending-cursor-type' is
+set instead.  Tools can consult this window parameter to restore the
+cursor type."
+  (cl-destructuring-bind (&key cursor face unhide) org-inside-appearance
+    (let* ((inside-p (and beg end))
+           (win (or win (selected-window)))
+           (state (org-inside--state win unhide face (not (null beg2))))
+           (ov (ois/ov state))
+           (showing-p (overlay-get ov 'invisible)) ; non-nil = unhidden!
+           (ov2 (ois/ov2 state)))
+      ;; more natural movement moving outside when hidden text is visible
+      (unless (or (not showing-p) inside-p)
+        (setq disable-point-adjustment t))
+
+      ;; handle (delayed) auto unhiding
+      (if inside-p
+          (when (and unhide
+                     (numberp org-inside-unhide-delay)
+                     (not (zerop org-inside-unhide-delay))
+                     (not org-inside--unhide-timer))
+            (overlay-put ov 'invisible nil)
+            (setq
+             org-inside--unhide-timer
+             (run-at-time
+              org-inside-unhide-delay nil
+              (lambda (o) (overlay-put o 'invisible 'org-inside--not-hidden))
+              ov)))
+        ;; User may have toggled hiding; reset it
+        (overlay-put ov 'invisible (and unhide 'org-inside--not-hidden)))
+
+      ;; Update the cursor type
+      (when cursor
+        (let ((cursor (if inside-p cursor
+                        (or (ois/saved-cursor-type state) t)))
+              (win-cursor-type (window-cursor-type win)))
+          (if (eq win-cursor-type nil)
+	      ;; Do not override a hidden (nil) cursor; set it pending instead
+              (set-window-parameter win 'pending-cursor-type cursor)
+            (unless (eq cursor win-cursor-type) ; already set?
+              (when inside-p                    ; save old type
+	        (setf (ois/saved-cursor-type state) win-cursor-type))
+              (set-window-cursor-type win cursor)))))
+      ;; Move the overlays into place, or remove them.  We do this
+      ;; when returning to the run-loop to avoid the cursor-sensor
+      ;; race for point adjustment.  This can happen since our overlay
+      ;; can unhide the very text point adjustment is skipping.
+      (run-at-time 0 nil
+                   (lambda (buf)
+                     (with-current-buffer buf
+                       (if inside-p (move-overlay ov beg end)
+                         (delete-overlay ov))
+                       (if beg2
+                           (move-overlay ov2 beg2 end2)
+                         (when (overlayp ov2) (delete-overlay ov2)))))
+                   (current-buffer)))))
+
+(defun org-inside--visible-region (elem)
+  "Return the visible region of entity ELEM.
+Returned region is a cons (BEG . END), or nil if no such region exists.."
+  (let  ((beg (org-element-begin elem))
+         (end (- (org-element-end elem) (org-element-post-blank elem))))
+    (when (get-text-property beg 'invisible)
+      (setq beg (next-single-property-change beg 'invisible nil end)
+            end (previous-single-property-change end 'invisible nil beg))
+      (and (> end beg) (cons beg end)))))
+
+(defun org-inside--sensor (win _pos type)
+  "Handle cursor appearance and unhiding inside entities with hidden contents.
+To be set via the `cursor-sensor-functions' property, as well as the
+overlay in each `org-inside-state' .  WIN POS, and TYPE are the window,
+former position, and cursor movement type."
+  (unless (minibuffer-window-active-p win)
+    (cond
+     ((or (eq type 'entered)   ; called from in-text cursor-sensor
+          (and (eq type 'moved) (plist-get org-inside-appearance :face)))
+      (when-let* ((elems (org-inside--elems-at-point)) ; ordered inner->outer
+                  (outer-elem (car (last elems))))
+        (let ((beg (org-element-begin outer-elem))
+              (end (- (org-element-end outer-elem)
+                      (org-element-post-blank outer-elem)))
+              beg2 end2)
+          (when (and (> (length elems) 1)        ; nested entities
+                     (>= emacs-major-version 31) ; needed for `moved'
+                     (plist-get org-inside-appearance :face))
+            (pcase (org-inside--visible-region (car elems))
+              ((and `(,b . ,e) (guard (and b e)))
+               (if (<= b (point) e) (setq beg2 b end2 e)
+                 ;; We are within a relevant inner org-element, but
+                 ;; outside its visible region.  Use the level above,
+                 ;; if any.
+                 (when (> (length elems) 2)
+                   (pcase-setq `(,beg2 . ,end2)
+                               (org-inside--visible-region (cadr elems))))))))
+          (org-inside--set-appearance win beg end beg2 end2))))
+     ((eq type 'left) ; called from the primary overlay's override cursor-sensor
+      (org-inside--set-appearance win)))
+    (when org-inside--unhide-timer
+      (cond
+       ((eq type 'moved)                ; reschedule
+        (timer-set-time org-inside--unhide-timer
+                        (timer-relative-time nil org-inside-unhide-delay)))
+       ((eq type 'left)
+        (cancel-timer org-inside--unhide-timer)
+        (setq org-inside--unhide-timer nil))))))
+
+(defun org-inside--buffer-changed (win)
+  "Handle `org-inside' buffers appearing or disappearing from window WIN."
+  (when org-inside-mode
+    (org-inside--trim-states)
+    (when (and (eq (current-buffer) (window-buffer win)) ; appearing
+               (memq 'org-inside--sensor
+                     (get-text-property (point) 'cursor-sensor-functions)))
+      (org-inside--sensor win nil 'entered))))
+
+(defun org-inside--frame-changed (frame)
+  "Handle `org-inside' buffers disappearing for all windows on FRAME.
+Not needed on v31+, as the buffer-local value of
+`window-buffer-change-functions' is called for window where the buffer
+either appeared or disappeared."
+  (walk-windows
+   (lambda (win)
+     (with-current-buffer (window-old-buffer win)
+       (org-inside--buffer-changed win))
+     nil frame)))
+
+(defun org-inside--add-properties (type _beg _end visible-beg visible-end)
+  "Add text properties to invisible text for org-inside functionality.
+TYPE is the type of text being hidden.  BEG, END, VISIBLE-BEG,
+VISIBLE-END are the buffer positions of the affected text and its
+visible portion.  To be set on `org-hidden-text-functions'."
+  ;; Emacs 31+ fires cursor-sensor at positions where an inserted
+  ;; character would inherit the `cursor-sensor-function' property
+  ;; (including the effects of rear stickiness).  Prior versions do
+  ;; not respect stickiness.  To get the same functionality in earlier
+  ;; versions, we include the next trailing (marker) char in the
+  ;; "inside" region, but do not inherit the sensor property for
+  ;; characters inserted after it.
+  (when (> visible-end (point-min))
+    (if (>= emacs-major-version 31) ;; TODO: use static-if when available
+        ;; rear-nonsticky=t in the char before visible end would prevent
+        ;; v31+ from recognizing the sensor, so we disable that.
+        (when (eq (get-text-property (1- visible-end) 'rear-nonsticky) t)
+          (remove-text-properties (1- visible-end) visible-end
+                                  '(rear-nonsticky t)))
+      (setq visible-end (min (1+ visible-end) (point-max)))
+      (add-text-properties (1- visible-end) visible-end
+                           '(rear-nonsticky (cursor-sensor-functions)))))
+
+  ;; for proper point adjustment
+  (when (memq type '(emphasis raise))
+    (org-rear-nonsticky-at visible-beg)
+    (when (< emacs-major-version 31)
+      (org-rear-nonsticky-at visible-end)))
+  (put-text-property visible-beg visible-end
+		     'cursor-sensor-functions '(org-inside--sensor)))
+
+(defvar-local org-inside--saved-fill-para-function nil)
+
+(defun org-inside--fill-paragaph (&rest r)
+  "Toggle hidden during fill paragraph.
+R are the provided arguments."
+  (interactive)
+  (if-let* ((state (cl-find (selected-window)
+                            org-inside--states :key #'ois/window))
+            (ov (ois/ov state))
+            (iprop (overlay-get ov 'invisible))) ; non-nil = VISIBLE
+      (progn
+        (overlay-put ov 'invisible nil) ; respect underlying 'invisible
+        (prog1
+            (apply org-inside--saved-fill-para-function r)
+          (overlay-put ov 'invisible iprop)))
+    (apply org-inside--saved-fill-para-function r)))
+
+(defun org-inside--setup ()
+  "Setup buffer for `org-inside'."
+  (cursor-sensor-mode 1)
+  (cl-pushnew 'cursor-sensor-functions
+              (buffer-local-value 'org-extra-unfontify-properties
+                                  (current-buffer)))
+  (add-hook 'window-buffer-change-functions #'org-inside--buffer-changed nil t)
+  (add-hook 'org-hidden-text-functions #'org-inside--add-properties nil t)
+  (add-hook 'org-ctrl-c-ctrl-c-hook #'org-inside-toggle-hidden nil t)
+  (when fill-paragraph-function
+    (setq org-inside--saved-fill-para-function fill-paragraph-function
+          fill-paragraph-function 'org-inside--fill-paragaph))
+  (font-lock-flush) ;; does not call sensor functions
+  (dolist (w (get-buffer-window-list nil nil t))
+    (org-inside--buffer-changed w))
+  (org-inside--setup-hidden-contents-types))
+
+(defun org-inside--teardown ()
+  "Tear down `org-inside-mode' in buffer."
+  (org-inside--trim-states 'all)
+  (cursor-sensor-mode -1)
+  (setq-local org-extra-unfontify-properties
+              (delq 'cursor-sensor-functions org-extra-unfontify-properties))
+  (when org-inside--saved-fill-para-function
+    (setq-local fill-paragraph-function org-inside--saved-fill-para-function))
+  (remove-hook 'org-ctrl-c-ctrl-c-hook #'org-inside-toggle-hidden t)
+  (remove-hook 'org-hidden-text-functions #'org-inside--add-properties t)
+  (remove-hook 'window-buffer-change-functions #'org-inside--buffer-changed t))
+
+(defun org-inside--reset-all ()
+  "Reset `org-inside' in all windows showing org-inside buffers."
+  (walk-windows
+   (lambda (win)
+     (with-selected-window win
+       (when org-inside-mode
+         (when-let* ((s (org-inside--state-for-window win)))
+           (org-inside--remove-state s))
+         (org-inside--buffer-changed win))))
+   nil t))
+
+(defun org-inside-toggle-hidden ()
+  "Toggle visibility of hidden text for outermost entity at point (if any).
+Operates only when inside an entity wrapped by hidden text.  Text will
+be re-hidden when point leaves the entity.  See `org-inside-appearance'
+to enable automatic unhiding or configure other appearance settings.
+Returns non-nil if the visibility was toggled, making it suitable for
+inclusion on `org-ctrl-c-ctrl-c-hook'."
+  (interactive)
+  (when (org-inside--hidden-element)
+    (cl-destructuring-bind (&key unhide face &allow-other-keys)
+        org-inside-appearance
+      (let* ((state (org-inside--state (selected-window) unhide face))
+             (ov (ois/ov state))
+             (inv (overlay-get ov 'invisible)))
+        (overlay-put ov 'invisible (if inv nil 'org-inside--not-hidden))
+        t))))
+
+(defvar org-inside-mode-map
+  (let ((map (make-sparse-keymap)))
+    (define-key map [remap fill-paragraph] #'org-inside--fill-paragaph)
+    map)
+  "Keymap for org-inside.")
+
+;;;###autoload
+(define-minor-mode org-inside-mode
+  "Change appearance when point is inside an entity wrapped by hidden text.
+The cursor type and/or text face can be altered when point is inside the
+hidden region.  \"Inside\" means any characters entered at that point
+will appear with the visible text.  For example, entering a character
+when \"inside\" underlined text would make the new character underlined
+as well.  I.e., for position `[x]':
+
+  [x]_underline_   ; outside
+  _[x]underline_   ; inside
+
+Hidden text can be unhidden, either automatically, or by using the
+command `org-inside-toggle-hidden'.  See `org-inside-appearance' to
+configure what appearance changes occur."
+  :global nil
+  (cond
+   ((not org-inside-mode) (org-inside--teardown))
+   ((not (cl-loop for key in org-inside-appearance by #'cddr
+                  always (memq key '(:cursor :face :unhide))))
+    (setq org-inside-mode nil)
+    (user-error "`org-inside-appearance' malformed"))
+   ((and (not org-hide-emphasis-markers) (not org-link-descriptive)
+         (not (and org-pretty-entities
+                   org-pretty-entities-include-sub-superscripts)))
+    (message "`org-inside' inactive without hidden elements."))
+   (org-inside-mode (org-inside--setup))))
+
+;; Starting in v31, buffer-local change functions are run in
+;; windows a buffer left as well those it entered.
+(unless (>= emacs-major-version 31)
+  (add-hook 'window-buffer-change-functions #'org-inside--frame-changed))
+
+(provide 'org-inside)
+;;; org-inside.el ends here
+;; Local Variables:
+;; read-symbol-shorthands: (("ois/" . "org-inside-state-"))
+;; End:
diff --git a/lisp/org.el b/lisp/org.el
index 0a2c0b362..faecfded7 100644
--- a/lisp/org.el
+++ b/lisp/org.el
@@ -5183,6 +5183,8 @@ org-mode
      'match-hash :read-related t))
   (org-set-regexps-and-options)
   (add-to-invisibility-spec '(org-link))
+  (add-to-invisibility-spec '(org-emphasis))
+  (add-to-invisibility-spec '(org-raise))
   (org-fold-initialize (or (and (stringp org-ellipsis) (not (equal "" org-ellipsis)) org-ellipsis)
                            "..."))
   (make-local-variable 'org-link-descriptive)
@@ -5450,6 +5452,24 @@ org-nonsticky-props
 (defsubst org-rear-nonsticky-at (pos)
   (add-text-properties (1- pos) pos (list 'rear-nonsticky org-nonsticky-props)))
 
+(defvar org-hidden-text-functions nil
+  "Abnormal hook called when hiding certain text during fontification.
+
+Each function on the hook should take five arguments:
+
+  TYPE: the type of text being hidden
+  BEG, END: the starting and ending buffer positions of the text
+  VBEG, VEND: the start and end of the visible portion of the text
+
+Currently supported types (symbols) are:
+
+  `emphasis': emphasized text with hidden markers, with non-nil
+              `org-hide-emphasis-markers'.
+
+  `link': bracket links, with non-nil `org-link-descriptive'.
+
+  `raise': sub/superscripts.")
+
 (defun org-do-emphasis-faces (limit)
   "Run through the buffer and emphasize strings."
   (let ((quick-re (format "\\([%s]\\|^\\)\\([~=*/_+]\\)"
@@ -5495,11 +5515,11 @@ org-do-emphasis-faces
 	      (when (and org-hide-emphasis-markers
 			 (not (org-at-comment-p)))
 		(add-text-properties (match-end 4) (match-beginning 5)
-				     '(invisible t))
+				     '(invisible org-emphasis))
                 ;; https://orgmode.org/list/[email protected]
                 (org-rear-nonsticky-at (match-beginning 5))
 		(add-text-properties (match-beginning 3) (match-end 3)
-				     '(invisible t))
+				     '(invisible org-emphasis))
                 ;; FIXME: This would break current behavior with point
                 ;; being adjusted before hidden emphasis marker when
                 ;; using M-b.  A proper fix would require custom
@@ -5507,7 +5527,9 @@ org-do-emphasis-faces
                 ;; word constituents where appropriate.
                 ;; https://orgmode.org/list/87edl41jf0.fsf@localhost
                 ;; (org-rear-nonsticky-at (match-end 3))
-                )
+                (run-hook-with-args 'org-hidden-text-functions 'emphasis
+                                    (match-beginning 2) (match-end 2)
+                                    (match-beginning 4) (match-end 4)))
 	      (throw :exit t))))))))
 
 (defun org-emphasize (&optional char)
@@ -5570,13 +5592,13 @@ org-activate-links
 	(if (and (memq style org-highlight-links)
 		 ;; Do not span over paragraph boundaries.
 		 (not (string-match-p org-element-paragraph-separate
-				    (match-string 0)))
+				      (match-string 0)))
 		 ;; Do not confuse plain links with tags.
 		 (not (and (eq style 'plain)
-			 (let ((face (get-text-property
-				      (max (1- start) (point-min)) 'face)))
-			   (if (consp face) (memq 'org-tag face)
-			     (eq 'org-tag face))))))
+			   (let ((face (get-text-property
+				        (max (1- start) (point-min)) 'face)))
+			     (if (consp face) (memq 'org-tag face)
+			       (eq 'org-tag face))))))
 	    (let* ((link-object (save-excursion
 				  (goto-char start)
 				  (save-match-data (org-element-link-parser))))
@@ -5622,7 +5644,10 @@ org-activate-links
 		  (add-text-properties visible-start visible-end properties)
 		  (add-text-properties visible-end end hidden)
 		  (org-rear-nonsticky-at visible-start)
-		  (org-rear-nonsticky-at visible-end)))
+		  (org-rear-nonsticky-at visible-end))
+                (when org-link-descriptive
+                  (run-hook-with-args 'org-hidden-text-functions 'link
+                                      start end visible-start visible-end)))
 	      (let ((f (org-link-get-parameter type :activate-func)))
 	        (when (functionp f)
 		  (funcall f start end path (eq style 'bracket))))
@@ -6379,16 +6404,21 @@ org-font-lock-add-tag-faces
 			   (list 'font-lock-fontified t))
       (backward-char 1))))
 
+(defvar org-extra-unfontify-properties nil
+  "Extra properties to unfontify.
+Specify as `(PROP1 t PROP2 t ...)'.")
+
 (defun org-unfontify-region (beg end &optional _maybe_loudly)
   "Remove fontification and activation overlays from links."
   (font-lock-default-unfontify-region beg end)
   (with-silent-modifications
     (decompose-region beg end)
     (remove-text-properties beg end
-			    '(mouse-face t keymap t org-linked-text t
+			    `(mouse-face t keymap t org-linked-text t
 					 invisible t intangible t
 					 org-emphasis t
-                                         syntax-table t))
+                                         syntax-table t
+                                         ,@org-extra-unfontify-properties))
     (org-fold-core-update-optimization beg end)
     (org-remove-font-lock-display-properties beg end)))
 
@@ -6419,10 +6449,11 @@ org-raise-scripts
 		org-match-substring-with-braces-regexp)
 	      limit t))
     (let* ((pos (point)) table-p comment-p
-	   (mpos (match-beginning 3))
-	   (emph-p (get-text-property mpos 'org-emphasis))
-	   (link-p (get-text-property mpos 'mouse-face))
-	   (keyw-p (eq 'org-special-keyword (get-text-property mpos 'face))))
+	   (vbeg (match-beginning 3)) (vend (match-end 3))
+	   (emph-p (get-text-property vbeg 'org-emphasis))
+	   (link-p (get-text-property vbeg 'mouse-face))
+	   (keyw-p (eq 'org-special-keyword (get-text-property vbeg 'face)))
+           (props '(invisible org-raise rear-nonsticky (invisible))))
       (goto-char (line-beginning-position))
       (setq table-p (looking-at-p org-table-dataline-regexp)
 	    comment-p (looking-at-p "^[ \t]*#[ +]"))
@@ -6430,21 +6461,19 @@ org-raise-scripts
       ;; Handle a_b^c
       (when (member (char-after) '(?_ ?^)) (goto-char (1- pos)))
       (unless (or comment-p emph-p link-p keyw-p)
-	(put-text-property (match-beginning 3) (match-end 0)
-			   'display
+	(put-text-property (match-beginning 2) vend 'org-emphasis t)
+        (add-text-properties (match-beginning 2) (match-end 2) props)
+	(when (and (eq (char-after vbeg) ?{)
+		   (eq (char-before vend) ?}))
+	  (add-text-properties vbeg (1+ vbeg) props)
+	  (add-text-properties (1- vend) vend props)
+          (setq vbeg (1+ vbeg) vend (1- vend)))
+	(put-text-property vbeg vend 'display
 			   (if (equal (char-after (match-beginning 2)) ?^)
 			       (nth (if table-p 3 1) org-script-display)
 			     (nth (if table-p 2 0) org-script-display)))
-        (put-text-property (match-beginning 2) (match-end 3)
-                           'org-emphasis t)
-	(add-text-properties (match-beginning 2) (match-end 2)
-			     (list 'invisible t))
-	(when (and (eq (char-after (match-beginning 3)) ?{)
-		   (eq (char-before (match-end 3)) ?}))
-	  (add-text-properties (match-beginning 3) (1+ (match-beginning 3))
-			       (list 'invisible t))
-	  (add-text-properties (1- (match-end 3)) (match-end 3)
-			       (list 'invisible t))))
+        (run-hook-with-args 'org-hidden-text-functions 'raise
+                            (match-beginning 0) (match-end 0) vbeg vend))
       t)))
 
 (defun org-remove-empty-overlays-at (pos)

Reply via email to