Chat log: Ragmacs test (Claude)

Elisp introspection tools for gptel


Prompt: @introspect Is there a way to ensure that all text inserted with insert in a specific Emacs buffer inherits text properties from its surroundings?

Note:

Response: Let me investigate the insert function and related text property mechanisms to find a solution.

(function_documentation :function “insert”)
(:name "function_documentation" :args (:function "insert"))

Insert the arguments, either strings or characters, at point.
Point and after-insertion markers move forward to end up
 after the inserted text.
Any other markers at the point of insertion remain before the text.

If the current buffer is multibyte, unibyte strings are converted
to multibyte for insertion (see ‘string-make-multibyte’).
If the current buffer is unibyte, multibyte strings are converted
to unibyte for insertion (see ‘string-make-unibyte’).

When operating on binary data, it may be necessary to preserve the
original bytes of a unibyte string when inserting it into a multibyte
buffer; to accomplish this, apply ‘decode-coding-string’ with the
‘no-conversion’ coding system to the string and insert the result.

(fn &rest ARGS)
(function_documentation :function "insert-and-i …)
(:name "function_documentation" :args (:function "insert-and-inherit"))

Insert the arguments at point, inheriting properties from adjoining text.
Point and after-insertion markers move forward to end up
 after the inserted text.
Any other markers at the point of insertion remain before the text.

If the current buffer is multibyte, unibyte strings are converted
to multibyte for insertion (see ‘unibyte-char-to-multibyte’).
If the current buffer is unibyte, multibyte strings are converted
to unibyte for insertion.

(fn &rest ARGS)

Let me look for buffer-local hooks or mechanisms that could intercept text insertion:

(variable_completions :variable_prefix "after-c …)
(:name "variable_completions" :args (:variable_prefix "after-change"))

bbdb-after-change-hook
jit-lock-after-change-extend-region-functions
font-lock-extend-after-change-region-function
after-change-functions
pdf-view-after-change-page-hook
after-change-major-mode-hook
combine-after-change-calls
org-attach-after-change-hook
c-in-after-change-fontification
(variable_documentation :variable "after-change …)
(:name "variable_documentation" :args (:variable "after-change-functions"))

List of functions to call after each text change.
Three arguments are passed to each function: the positions of
the beginning and end of the range of changed text,
and the length in chars of the pre-change text replaced by that range.
(For an insertion, the pre-change length is zero;
for a deletion, that length is the number of chars deleted,
and the post-change beginning and end are at the same place.)

Buffer changes made while executing the `after-change-functions'
don't call any before-change or after-change functions.
That's because `inhibit-modification-hooks' is temporarily set non-nil.

If an unhandled error happens in running these functions,
the variable's value remains nil.  That prevents the error
from happening repeatedly and making Emacs nonfunctional.

Perfect! The after-change-functions hook runs after text changes and receives the positions. Let me check if there are text property functions that can help:

(function_completions :function_prefix "text-pr …)
(:name "function_completions" :args (:function_prefix "text-properties-at"))

text-properties-at
fill-text-properties-at
(function_documentation :function "text-propert …)
(:name "function_documentation" :args (:function "text-properties-at"))

Return the list of properties of the character at POSITION in OBJECT.
If the optional second argument OBJECT is a buffer (or nil, which means
the current buffer), POSITION is a buffer position (integer or marker).

If OBJECT is a string, POSITION is a 0-based index into it.

If POSITION is at the end of OBJECT, the value is nil, but note that
buffer narrowing does not affect the value.  That is, if OBJECT is a
buffer or nil, and the buffer is narrowed and POSITION is at the end
of the narrowed buffer, the result may be non-nil.

If you want to display the text properties at point in a human-readable
form, use the ‘describe-text-properties’ command.

(fn POSITION &optional OBJECT)
(function_completions :function_prefix "set-tex …)
(:name "function_completions" :args (:function_prefix "set-text-properties"))

set-text-properties
(function_documentation :function "set-text-pro …)
(:name "function_documentation" :args (:function "set-text-properties"))

Completely replace properties of text from START to END.
The third argument PROPERTIES is the new property list.
If the optional fourth argument OBJECT is a buffer (or nil, which means
the current buffer), START and END are buffer positions (integers or
markers).  If OBJECT is a string, START and END are 0-based indices into it.
If PROPERTIES is nil, the effect is to remove all properties from
the designated part of OBJECT.

(fn START END PROPERTIES &optional OBJECT)

This response was truncated here because Anthropic rate-limited the exchange. I had to wait for a minute before continuing:


Prompt: @introspect Continue your analysis and find a solution.

Response: Let me check for functions that can add text properties to existing text:

(function_completions :function_prefix “add-text-properties”)
(:name "function_completions" :args (:function_prefix "add-text-properties"))

add-text-properties
gnus-add-text-properties-when
(function_documentation :function “add-text-properties”)
(:name "function_documentation" :args (:function "add-text-properties"))

Add properties to the text from START to END.
The third argument PROPERTIES is a property list
specifying the property values to add.  If the optional fourth argument
OBJECT is a buffer (or nil, which means the current buffer),
START and END are buffer positions (integers or markers).
If OBJECT is a string, START and END are 0-based indices into it.
Return t if any property value actually changed, nil otherwise.

(fn START END PROPERTIES &optional OBJECT)

Now let me look for functions that can get properties from adjacent text:

(function_completions :function_prefix “get-text-property”)
(:name "function_completions" :args (:function_prefix "get-text-property"))

gnus-get-text-property-excluding-characters-with-faces
get-text-property
(function_documentation :function “get-text-property”)
(:name "function_documentation" :args (:function "get-text-property"))

Return the value of POSITION’s property PROP, in OBJECT.
OBJECT should be a buffer or a string; if omitted or nil, it defaults
to the current buffer.

If POSITION is at the end of OBJECT, the value is nil, but note that
buffer narrowing does not affect the value.  That is, if the buffer is
narrowed and POSITION is at the end of the narrowed buffer, the result
may be non-nil.

(fn POSITION PROP &optional OBJECT)

Perfect! Here’s the solution:

You can use a buffer-local after-change-functions hook to detect insertions and apply inherited text properties. Here’s how:

(elisp_eval :expression "(defun make-insert-inherit-properties () …)
(:name "elisp_eval" :args (:expression "(defun make-insert-inherit-properties ()
  \"Make all insertions in current buffer inherit text properties.\"
  (add-hook 'after-change-functions 
            #'apply-inherited-properties-to-insertion 
            nil t))"))

make-insert-inherit-properties
(elisp_eval :expression "(defun apply-inherited-properties-to-ins …)
(:name "elisp_eval" :args (:expression "(defun apply-inherited-properties-to-insertion (beg end pre-change-length)
  \"Apply inherited text properties to inserted text.
BEG and END are the bounds of the change.
PRE-CHANGE-LENGTH is the length of text before the change.\"
  (when (zerop pre-change-length) ; This was an insertion
    (let ((props (cond
                  ;; Get properties from character before insertion point
                  ((> beg (point-min))
                   (text-properties-at (1- beg)))
                  ;; Get properties from character after insertion point
                  ((< end (point-max))
                   (text-properties-at end))
                  ;; No surrounding text
                  (t nil))))
      (when props
        (set-text-properties beg end props)))))"))

apply-inherited-properties-to-insertion

To use this solution:

  1. Call (make-insert-inherit-properties) in the target buffer
  2. All subsequent insert calls in that buffer will inherit properties from surrounding text

The hook detects insertions by checking if pre-change-length is zero, then applies properties from the character before the insertion point (or after, if at buffer start).

This is buffer-local and doesn’t affect insert globally.


Prompt: @introspect Okay, now let’s reduce the scope to one property: gptel. The value of only this property should be applied. Further, this property is front-sticky and rear-nonsticky, and we want to retain that behavior when text is inserted.

Response: Let me check the functions for handling sticky properties and getting specific properties:

(function_completions :function_prefix “get-text-property”)
(:name "function_completions" :args (:function_prefix "get-text-property"))

gnus-get-text-property-excluding-characters-with-faces
get-text-property
(function_completions :function_prefix “put-text-property”)
(:name "function_completions" :args (:function_prefix "put-text-property"))

gnus-put-text-property-excluding-characters-with-faces
put-text-property
gnus-put-text-property-excluding-newlines
(function_documentation :function “put-text-property”)
(:name "function_documentation" :args (:function "put-text-property"))

Set one property of the text from START to END.
The third and fourth arguments PROPERTY and VALUE
specify the property to add.
If the optional fifth argument OBJECT is a buffer (or nil, which means
the current buffer), START and END are buffer positions (integers or
markers).  If OBJECT is a string, START and END are 0-based indices into it.

(fn START END PROPERTY VALUE &optional OBJECT)

Now let me check how sticky properties work:

(manual_names nil)
(:name "manual_names" :args nil)

["cl","sc","org","fdl","gpl","ede","edt","epa","erc","ert","eww","gdb","ido","pgg","ses","url","vip","sed","tar","zsh","ghub","dash","auth","calc","dbus","efaq","eudc","gnus","info","mh-e","sasl","ssip","stow","bash","cpio","find","gawk","grep","gzip","nano","time","wget","forge","magit","ediff","eglot","eieio","eintr","elisp","emacs","forms","gnupg","msmtp","rcirc","sieve","stabs","tramp","viper","woman","which","org-ql","geiser","embark","auctex","elpaca","aspell","bovine","ccmode","eshell","reftex","vtable","widget","wisent","mtools","company","dired-x","ebrowse","flymake","idlwave","message","pcl-cvs","spd-say","srecode","texinfo","pm-gawk","org-node","org-roam","orgguide","annotate","autotype","ctf-spec","pinentry","remember","semantic","smtpmail","speedbar","gawkinet","orderless","transient","info-stnd","mairix-el","nxml-mode","todo-mode","vhdl-mode","coreutils","diffutils","libext2fs","doclicense","aspell-dev","emacs-mime","newsticker","find-maint","with-editor","htmlfontify","octave-mode","sframe-spec","use-package","modus-themes","org-glossary","geiser-guile","emacs-gnutls","texi2any_api","gawkworkflow","magit-section","preview-latex","org-super-agenda","speech-dispatcher","texi2any_internals","speech-dispatcher-cs"]
(manual_node_contents :manual_name “elisp” :node “Sticky Properties”)
(:name "manual_node_contents" :args (:manual_name "elisp" :node "Sticky Properties"))

File: elisp.info,  Node: Sticky Properties,  Next: Lazy Properties,  Prev: Format Properties,  Up: Text Properties

33.19.6 Stickiness of Text Properties
-----

Self-inserting characters, the ones that get inserted into a buffer when
the user types them (*note Commands for Insertion::), normally take on
the same properties as the preceding character.  This is called
“inheritance” of properties.

   By contrast, a Lisp program can do insertion with inheritance or
without, depending on the choice of insertion primitive.  The ordinary
text insertion functions, such as ‘insert’, do not inherit any
properties.  They insert text with precisely the properties of the
string being inserted, and no others.  This is correct for programs that
copy text from one context to another--for example, into or out of the
kill ring.  To insert with inheritance, use the special primitives
described in this section.  Self-inserting characters inherit properties
because they work using these primitives.

   When you do insertion with inheritance, _which_ properties are
inherited, and from where, depends on which properties are “sticky”.
Insertion after a character inherits those of its properties that are
“rear-sticky”.  Insertion before a character inherits those of its
properties that are “front-sticky”.  When both sides offer different
sticky values for the same property, the previous character's value
takes precedence.

   By default, a text property is rear-sticky but not front-sticky;
thus, the default is to inherit all the properties of the preceding
character, and nothing from the following character.

   You can control the stickiness of various text properties with two
specific text properties, ‘front-sticky’ and ‘rear-nonsticky’, and with
the variable ‘text-property-default-nonsticky’.  You can use the
variable to specify a different default for a given property.  You can
use those two text properties to make any specific properties sticky or
nonsticky in any particular part of the text.

   If a character's ‘front-sticky’ property is ‘t’, then all its
properties are front-sticky.  If the ‘front-sticky’ property is a list,
then the sticky properties of the character are those whose names are in
the list.  For example, if a character has a ‘front-sticky’ property
whose value is ‘(face read-only)’, then insertion before the character
can inherit its ‘face’ property and its ‘read-only’ property, but no
others.

   The ‘rear-nonsticky’ property works the opposite way.  Most
properties are rear-sticky by default, so the ‘rear-nonsticky’ property
says which properties are _not_ rear-sticky.  If a character's
‘rear-nonsticky’ property is ‘t’, then none of its properties are
rear-sticky.  If the ‘rear-nonsticky’ property is a list, properties are
rear-sticky _unless_ their names are in the list.

-- Variable: text-property-default-nonsticky
    This variable holds an alist which defines the default
    rear-stickiness of various text properties.  Each element has the
    form ‘(PROPERTY . NONSTICKINESS)’, and it defines the stickiness of
    a particular text property, PROPERTY.

If NONSTICKINESS is non-‘nil’, this means that the property
PROPERTY is rear-nonsticky by default.  Since all properties are
front-nonsticky by default, this makes PROPERTY nonsticky in both
directions by default.

The text properties ‘front-sticky’ and ‘rear-nonsticky’, when used,
take precedence over the default NONSTICKINESS specified in
‘text-property-default-nonsticky’.

   Here are the functions that insert text with inheritance of
properties:

-- Function: insert-and-inherit &rest strings
    Insert the strings STRINGS, just like the function ‘insert’, but
    inherit any sticky properties from the adjoining text.

-- Function: insert-before-markers-and-inherit &rest strings
    Insert the strings STRINGS, just like the function
    ‘insert-before-markers’, but inherit any sticky properties from the
    adjoining text.

   ,*Note Insertion::, for the ordinary insertion functions which do not
inherit.

Perfect! Now I understand the sticky property mechanism. Let me create a focused solution for the gptel property:

(elisp_eval :expression "(defun make-insert-inherit-gptel-propert …)
(:name "elisp_eval" :args (:expression "(defun make-insert-inherit-gptel-property ()
  \"Make all insertions in current buffer inherit the gptel property with stickiness.\"
  (add-hook 'after-change-functions 
            #'apply-inherited-gptel-property 
            nil t))"))

make-insert-inherit-gptel-property
(elisp_eval :expression "(defun apply-inherited-gptel-property (b …)
(:name "elisp_eval" :args (:expression "(defun apply-inherited-gptel-property (beg end pre-change-length)
  \"Apply inherited gptel property to inserted text with proper stickiness.
BEG and END are the bounds of the change.
PRE-CHANGE-LENGTH is the length of text before the change.\"
  (when (zerop pre-change-length) ; This was an insertion
    (let ((gptel-value nil)
          (front-sticky-value nil)
          (rear-nonsticky-value nil))
      ;; Check for rear-sticky gptel property from preceding character
      (when (> beg (point-min))
        (let ((prev-pos (1- beg)))
          (setq gptel-value (get-text-property prev-pos 'gptel))
          (when gptel-value
            ;; Check if gptel is rear-sticky (default) or explicitly rear-nonsticky
            (let ((rear-nonsticky (get-text-property prev-pos 'rear-nonsticky)))
              (when (or (not rear-nonsticky)
                       (and (listp rear-nonsticky) 
                            (not (memq 'gptel rear-nonsticky)))
                       (and (not (eq rear-nonsticky t))))
                ;; Inherit the stickiness properties too
                (setq front-sticky-value (get-text-property prev-pos 'front-sticky))
                (setq rear-nonsticky-value (get-text-property prev-pos 'rear-nonsticky)))))))

;; If no rear-sticky gptel found, check for front-sticky from following character
(when (and (not gptel-value) (< end (point-max)))
  (let ((next-pos end))
    (let ((front-sticky (get-text-property next-pos 'front-sticky)))
      (when (or (eq front-sticky t)
               (and (listp front-sticky) (memq 'gptel front-sticky)))
        (setq gptel-value (get-text-property next-pos 'gptel))
        (when gptel-value
          (setq front-sticky-value (get-text-property next-pos 'front-sticky))
          (setq rear-nonsticky-value (get-text-property next-pos 'rear-nonsticky)))))))

;; Apply the properties if we found a gptel value
(when gptel-value
  (put-text-property beg end 'gptel gptel-value)
  (when front-sticky-value
    (put-text-property beg end 'front-sticky front-sticky-value))
  (when rear-nonsticky-value
    (put-text-property beg end 'rear-nonsticky rear-nonsticky-value))))))"))

apply-inherited-gptel-property

This solution:

  1. Only inherits the gptel property (not all properties)
  2. Respects stickiness rules:
    • Checks rear-sticky inheritance from preceding character (default behavior)
    • Falls back to front-sticky inheritance from following character if gptel is front-sticky there
  3. Preserves stickiness: Also copies the front-sticky and rear-nonsticky properties that control the gptel property’s stickiness behavior

To use: Call (make-insert-inherit-gptel-property) in the target buffer. All subsequent insert calls will inherit the gptel property with proper sticky behavior.


Prompt: Provide the code for make-insert-inherit-gptel-property and apply-inherited-gptel-property.

Response:

(defun make-insert-inherit-gptel-property ()
  "Make all insertions in current buffer inherit the gptel property with stickiness."
  (add-hook 'after-change-functions 
            #'apply-inherited-gptel-property 
            nil t))

(defun apply-inherited-gptel-property (beg end pre-change-length)
  "Apply inherited gptel property to inserted text with proper stickiness.
BEG and END are the bounds of the change.
PRE-CHANGE-LENGTH is the length of text before the change."
  (when (zerop pre-change-length) ; This was an insertion
    (let ((gptel-value nil)
          (front-sticky-value nil)
          (rear-nonsticky-value nil))
      ;; Check for rear-sticky gptel property from preceding character
      (when (> beg (point-min))
        (let ((prev-pos (1- beg)))
          (setq gptel-value (get-text-property prev-pos 'gptel))
          (when gptel-value
            ;; Check if gptel is rear-sticky (default) or explicitly rear-nonsticky
            (let ((rear-nonsticky (get-text-property prev-pos 'rear-nonsticky)))
              (when (or (not rear-nonsticky)
                       (and (listp rear-nonsticky) 
                            (not (memq 'gptel rear-nonsticky)))
                       (and (not (eq rear-nonsticky t))))
                ;; Inherit the stickiness properties too
                (setq front-sticky-value (get-text-property prev-pos 'front-sticky))
                (setq rear-nonsticky-value (get-text-property prev-pos 'rear-nonsticky)))))))
      
      ;; If no rear-sticky gptel found, check for front-sticky from following character
      (when (and (not gptel-value) (< end (point-max)))
        (let ((next-pos end))
          (let ((front-sticky (get-text-property next-pos 'front-sticky)))
            (when (or (eq front-sticky t)
                     (and (listp front-sticky) (memq 'gptel front-sticky)))
              (setq gptel-value (get-text-property next-pos 'gptel))
              (when gptel-value
                (setq front-sticky-value (get-text-property next-pos 'front-sticky))
                (setq rear-nonsticky-value (get-text-property next-pos 'rear-nonsticky)))))))
      
      ;; Apply the properties if we found a gptel value
      (when gptel-value
        (put-text-property beg end 'gptel gptel-value)
        (when front-sticky-value
          (put-text-property beg end 'front-sticky front-sticky-value))
        (when rear-nonsticky-value
          (put-text-property beg end 'rear-nonsticky rear-nonsticky-value))))))

Prompt: Thanks for the idea. I control the gptel text property, and I know that it will be front-sticky anywhere it’s applied. This is how I use it everywhere:

(add-text-properties
 0 (length response) '(gptel response front-sticky (gptel)) response)

So I used your idea to reduce your function to just this:

(defun gptel--inherit-from-end (beg end pre)
  (and (= pre 0) (< end (point-max))
       (and-let* ((val (get-text-property end 'gptel)))
         (add-text-properties
          beg end `(gptel ,val front-sticky (gptel))))))
(add-hook 'after-change-functions 'gptel--inherit-from-end nil t)

Any problems with this?

Response: No problems with your simplified version! It’s much cleaner and works correctly for your specific use case.

Your logic is sound:

The only minor consideration: if someone inserts at the very end of the buffer (end equals point-max), no inheritance occurs. But that’s probably the correct behavior since there’s no following character to inherit from.

Your version is more efficient and targeted than my general-purpose solution.


Prompt: Thanks.

Author: Karthik Chikmagalur

Created: 2025-08-02 Sat 14:02

Validate