Chat log: Ragmacs test (Claude)
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:
- I can’t use the dedicated
insert-and-inherit
function because I don’t control the caller ofinsert
in this use case. - I can’t advise
insert
because this should only apply to one buffer.
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:
- Call
(make-insert-inherit-properties)
in the target buffer - 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:
- Only inherits the
gptel
property (not all properties) - 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
- Preserves stickiness: Also copies the
front-sticky
andrear-nonsticky
properties that control thegptel
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:
- Since
gptel
is always front-sticky where applied, inserted text should inherit from the character after the insertion point - You only need to check
(< end (point-max))
since front-sticky inheritance comes from the following character and-let*
handles the nil check elegantly- You’re correctly preserving the front-sticky behavior by always applying
front-sticky (gptel)
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.