branch: elpa/gptel
commit 4ab198a904f1706a8daede1145386db4dc960aa1
Author: Karthik Chikmagalur <karthikchikmaga...@gmail.com>
Commit: Karthik Chikmagalur <karthikchikmaga...@gmail.com>

    gptel: Change buffer parser property-search method
    
    Parse the buffer using `previous-single-property-change' instead
    of `text-property-search-backward'.  This is for compatibility
    with overlay-based response bounds tracking.  In the future, we
    want to be able to use both overlays and text-properties to track
    responses since they can compensate for each other's weaknesses.
    
    Specifically, overlays handle insertions in the middle of response
    regions correctly, but are fragile under text deletion.  Text
    properties are robust but cannot "swallow" changes to regions
    consistently without adding to expensive hooks like
    `yank-transform-functions' and `after-change-functions'.
    
    * gptel-openai.el (gptel--parse-buffer): Above described changes.
    
    * gptel-ollama.el (gptel--parse-buffer): Above described changes.
    
    * gptel-gemini.el (gptel--parse-buffer): Above described changes.
    
    * gptel-anthropic.el (gptel--parse-buffer): Above described
    changes.
---
 gptel-anthropic.el | 51 +++++++++++++++++++++++----------------------------
 gptel-gemini.el    | 49 +++++++++++++++++++++++--------------------------
 gptel-ollama.el    | 47 ++++++++++++++++++++++-------------------------
 gptel-openai.el    | 53 +++++++++++++++++++++++++----------------------------
 4 files changed, 93 insertions(+), 107 deletions(-)

diff --git a/gptel-anthropic.el b/gptel-anthropic.el
index 50842e8c06..bc610929f3 100644
--- a/gptel-anthropic.el
+++ b/gptel-anthropic.el
@@ -269,42 +269,37 @@ TOOL-USE is a list of plists containing tool names, 
arguments and call results."
                  :content `[(:type "text" :text ,text)])))
 
 (cl-defmethod gptel--parse-buffer ((_backend gptel-anthropic) &optional 
max-entries)
-  (let ((prompts) (prop) (prev-pt (point))
+  (let ((prompts) (prev-pt (point))
         (include-media (and gptel-track-media (or (gptel--model-capable-p 
'media)
-                                                (gptel--model-capable-p 
'url)))))
+                                                  (gptel--model-capable-p 
'url)))))
     (if (or gptel-mode gptel-track-response)
-        (while (and
-                (or (not max-entries) (>= max-entries 0))
-                (setq prop (text-property-search-backward
-                            'gptel 'response
-                            (when (get-char-property (max (point-min) (1- 
(point)))
-                                                     'gptel)
-                              t))))
+        (while (and (or (not max-entries) (>= max-entries 0))
+                    (goto-char (previous-single-property-change
+                                (point) 'gptel nil (point-min)))
+                    (not (= (point) prev-pt)))
           ;; HACK Until we can find a more robust solution for editing
           ;; responses, ignore prompts containing only whitespace, as the
           ;; Anthropic API can't handle it.  See #452, #409, #406, #351 and 
#321
           ;; We check for blank prompts by skipping whitespace and comparing
           ;; point against the previous.
           (unless (save-excursion (skip-syntax-forward " ") (>= (point) 
prev-pt))
-            (if (prop-match-value prop) ; assistant role
-                (push (list :role "assistant"
-                            :content
-                            (buffer-substring-no-properties 
(prop-match-beginning prop)
-                                                            (prop-match-end 
prop)))
-                      prompts)
-              (if include-media         ; user role: possibly with media
-                  (push (list :role "user"
-                              :content
-                              (gptel--anthropic-parse-multipart
-                               (gptel--parse-media-links
-                                major-mode (prop-match-beginning prop) 
(prop-match-end prop))))
-                        prompts)
-                (push (list :role "user"
-                            :content
-                            (gptel--trim-prefixes
-                             (buffer-substring-no-properties 
(prop-match-beginning prop)
-                                                             (prop-match-end 
prop))))
-                      prompts))))
+            (pcase (get-char-property (point) 'gptel)
+              ('response
+               (push (list :role "assistant"
+                           :content (buffer-substring-no-properties (point) 
prev-pt))
+                     prompts))
+              ('nil                     ; user role: possibly with media
+               (if include-media       
+                   (push (list :role "user"
+                               :content
+                               (gptel--anthropic-parse-multipart
+                                (gptel--parse-media-links major-mode (point) 
prev-pt)))
+                         prompts)
+                 (push (list :role "user"
+                             :content
+                             (gptel--trim-prefixes
+                              (buffer-substring-no-properties (point) 
prev-pt)))
+                       prompts)))))
           (setq prev-pt (point))
           (and max-entries (cl-decf max-entries)))
       (push (list :role "user"
diff --git a/gptel-gemini.el b/gptel-gemini.el
index 1e69e82a75..81dc78c53d 100644
--- a/gptel-gemini.el
+++ b/gptel-gemini.el
@@ -226,35 +226,32 @@ See generic implementation for full documentation."
            finally return prompts))
 
 (cl-defmethod gptel--parse-buffer ((_backend gptel-gemini) &optional 
max-entries)
-  (let ((prompts) (prop)
+  (let ((prompts) (prev-pt (point))
         (include-media (and gptel-track-media (or (gptel--model-capable-p 
'media)
                                                   (gptel--model-capable-p 
'url)))))
     (if (or gptel-mode gptel-track-response)
-        (while (and
-                (or (not max-entries) (>= max-entries 0))
-                (setq prop (text-property-search-backward
-                            'gptel 'response
-                            (when (get-char-property (max (point-min) (1- 
(point)))
-                                                     'gptel)
-                              t))))
-          (if (prop-match-value prop)   ;assistant role
-              (push (list :role "model"
-                          :parts
-                          (list :text (buffer-substring-no-properties 
(prop-match-beginning prop)
-                                                                      
(prop-match-end prop))))
-                    prompts)
-            (if include-media
-                (push (list :role "user"
-                            :parts (gptel--gemini-parse-multipart
-                                    (gptel--parse-media-links
-                                     major-mode (prop-match-beginning prop) 
(prop-match-end prop))))
-                      prompts)
-              (push (list :role "user"
-                          :parts
-                          `[(:text ,(gptel--trim-prefixes
-                                     (buffer-substring-no-properties 
(prop-match-beginning prop)
-                                      (prop-match-end prop))))])
-                    prompts)))
+        (while (and (or (not max-entries) (>= max-entries 0))
+                    (goto-char (previous-single-property-change
+                                (point) 'gptel nil (point-min)))
+                    (not (= (point) prev-pt)))
+          (pcase (get-char-property (point) 'gptel)
+            ('response
+             (push (list :role "model"
+                         :parts
+                         (list :text (buffer-substring-no-properties (point) 
prev-pt)))
+                   prompts))
+            ('nil
+             (if include-media
+                 (push (list :role "user"
+                             :parts (gptel--gemini-parse-multipart
+                                     (gptel--parse-media-links major-mode 
(point) prev-pt)))
+                       prompts)
+               (push (list :role "user"
+                           :parts
+                           `[(:text ,(gptel--trim-prefixes
+                                      (buffer-substring-no-properties (point) 
prev-pt)))])
+                     prompts))))
+          (setq prev-pt (point))
           (and max-entries (cl-decf max-entries)))
       (push (list :role "user"
                   :parts
diff --git a/gptel-ollama.el b/gptel-ollama.el
index 97d3363942..9061786dc1 100644
--- a/gptel-ollama.el
+++ b/gptel-ollama.el
@@ -142,34 +142,31 @@ Store response metadata in state INFO."
            (list :role (if role "user" "assistant") :content text)))
 
 (cl-defmethod gptel--parse-buffer ((_backend gptel-ollama) &optional 
max-entries)
-  (let ((prompts) (prop)
+  (let ((prompts) (prev-pt (point))
         (include-media (and gptel-track-media (or (gptel--model-capable-p 
'media)
                                                   (gptel--model-capable-p 
'url)))))
     (if (or gptel-mode gptel-track-response)
-        (while (and
-                (or (not max-entries) (>= max-entries 0))
-                (setq prop (text-property-search-backward
-                            'gptel 'response
-                            (when (get-char-property (max (point-min) (1- 
(point)))
-                                                     'gptel)
-                              t))))
-          (if (prop-match-value prop)   ;assistant role
-              (push (list :role "assistant"
-                          :content (buffer-substring-no-properties 
(prop-match-beginning prop)
-                                                                   
(prop-match-end prop)))
-                    prompts)
-            (if include-media
-                (push (append '(:role "user")
-                             (gptel--ollama-parse-multipart
-                              (gptel--parse-media-links
-                               major-mode (prop-match-beginning prop) 
(prop-match-end prop))))
-                      prompts)
-              (push (list :role "user"
-                          :content
-                          (gptel--trim-prefixes
-                           (buffer-substring-no-properties 
(prop-match-beginning prop)
-                                                           (prop-match-end 
prop))))
-                    prompts)))
+        (while (and (or (not max-entries) (>= max-entries 0))
+                    (goto-char (previous-single-property-change
+                                (point) 'gptel nil (point-min)))
+                    (not (= (point) prev-pt)))
+          (pcase (get-char-property (point) 'gptel)
+            ('response
+             (push (list :role "assistant"
+                         :content (buffer-substring-no-properties (point) 
prev-pt))
+                   prompts))
+            ('nil
+             (if include-media
+                 (push (append '(:role "user")
+                               (gptel--ollama-parse-multipart
+                                (gptel--parse-media-links major-mode (point) 
prev-pt)))
+                       prompts)
+               (push (list :role "user"
+                           :content
+                           (gptel--trim-prefixes
+                            (buffer-substring-no-properties (point) prev-pt)))
+                     prompts))))
+          (setq prev-pt (point))
           (and max-entries (cl-decf max-entries)))
       (push (list :role "user"
                   :content
diff --git a/gptel-openai.el b/gptel-openai.el
index 5126941de3..34ef05c78d 100644
--- a/gptel-openai.el
+++ b/gptel-openai.el
@@ -319,41 +319,38 @@ Mutate state INFO with response metadata."
            (list :role (if role "user" "assistant") :content text)))
 
 (cl-defmethod gptel--parse-buffer ((_backend gptel-openai) &optional 
max-entries)
-  (let ((prompts) (prop)
+  (let ((prompts) (prev-pt (point))
         (include-media (and gptel-track-media
                             (or (gptel--model-capable-p 'media)
                                 (gptel--model-capable-p 'url)))))
     (if (or gptel-mode gptel-track-response)
-        (while (and
-                (or (not max-entries) (>= max-entries 0))
-                (setq prop (text-property-search-backward
-                            'gptel 'response
-                            (when (get-char-property (max (point-min) (1- 
(point)))
-                                                     'gptel)
-                              t))))
-          (if (prop-match-value prop)   ;assistant role
-              (push (list :role "assistant"
-                          :content
-                          (buffer-substring-no-properties 
(prop-match-beginning prop)
-                                                          (prop-match-end 
prop)))
-                    prompts)
-            (if include-media
-                (push (list :role "user"
-                            :content
-                            (gptel--openai-parse-multipart
-                             (gptel--parse-media-links
-                              major-mode (prop-match-beginning prop) 
(prop-match-end prop))))
-                      prompts)
-              (push (list :role "user"
-                          :content
-                          (gptel--trim-prefixes
-                           (buffer-substring-no-properties 
(prop-match-beginning prop)
-                                                           (prop-match-end 
prop))))
-                    prompts)))
+        (while (and (or (not max-entries) (>= max-entries 0))
+                    (/= prev-pt (point-min))
+                    (goto-char (previous-single-property-change
+                                (point) 'gptel nil (point-min))))
+          (pcase (get-char-property (point) 'gptel)
+            ('response
+             (push (list :role "assistant"
+                         :content (buffer-substring-no-properties (point) 
prev-pt))
+                   prompts))
+            ('nil
+             (if include-media
+                 (push (list :role "user"
+                             :content
+                             (gptel--openai-parse-multipart
+                              (gptel--parse-media-links major-mode (point) 
prev-pt)))
+                       prompts)
+               (push (list :role "user"
+                           :content
+                           (gptel--trim-prefixes
+                            (buffer-substring-no-properties (point) prev-pt)))
+                     prompts))))
+          (setq prev-pt (point))
           (and max-entries (cl-decf max-entries)))
       (push (list :role "user"
                   :content
-                  (gptel--trim-prefixes (buffer-substring-no-properties 
(point-min) (point-max))))
+                  (gptel--trim-prefixes (buffer-substring-no-properties
+                                         (point-min) (point-max))))
             prompts))
     prompts))
 

Reply via email to