branch: elpa/gptel
commit ef9684206a5c1a6a940f3df01bf51b82e93467e8
Author: daedsidog <[email protected]>
Commit: karthink <[email protected]>

    gptel-context: Add contexter
    
    gptel-contexter.el: New file for managing gptel's context for LLM
    queries.
    
    gptel-transient.el: Corresponding menu changes for context
    management.
---
 gptel-contexter.el | 356 +++++++++++++++++++++++++++++++++++++++++++++++++++
 gptel-transient.el | 364 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 gptel.el           |  26 ++++
 3 files changed, 746 insertions(+)

diff --git a/gptel-contexter.el b/gptel-contexter.el
new file mode 100644
index 0000000000..43cb8b55bd
--- /dev/null
+++ b/gptel-contexter.el
@@ -0,0 +1,356 @@
+
+;;; gptel-contexter.el --- Context aggregator for GPTel
+
+;; Copyright (C) 2023  Karthik Chikmagalur
+
+;; Author: daedsidog <[email protected]>
+;; Keywords: convenience, buffers
+
+;; SPDX-License-Identifier: GPL-3.0-or-later
+
+;; This program 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.
+
+;; This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; The contexter allows you to conveniently create contexts which can be fed
+;; to GPTel.
+
+;;; Code:
+
+;;; -*- lexical-binding: t -*-
+
+(require 'cl-lib)
+
+(defcustom gptel-context-highlight-face 'header-line
+  "Face to use to highlight selected context in the buffers."
+  :group 'gptel
+  :type 'symbol)
+
+(defcustom gptel-use-context-in-chat nil
+  "Determines if context should be injected when using the dedicated chat 
buffer."
+  :group 'gptel
+  :type 'symbol)
+
+(defcustom gptel-context-injection-destination :nowhere
+  "Where to inject the context.  Currently supported options are:
+
+    :nowhere               - Do not use the context at all.
+    :before-system-message - Inject the context right before the system 
message.
+    :after-system-message  - Inject the context right after the system emssage.
+    :before-user-prompt    - Inject the context right before the user prompt.
+    :after-user-prompt     - Inject the context right after the user prompt."
+  :group 'gptel
+  :type 'symbol)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; ------------------------------ FUNCTIONS ------------------------------- 
;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;;;###autoload
+(defun gptel-add-context (&optional arg)
+  "Add context to GPTel.
+
+When called regularly, adds current buffer as context.
+When ARG is positive, prompts for buffer name to add as context.
+When ARG is negative, removes current buffer from context.
+When called with region selected, adds selected region as context."
+  (interactive)
+  (cond
+   ;; A region is selected.
+   ((use-region-p)
+    (gptel--add-region-as-context (current-buffer)
+                                  (region-beginning)
+                                  (region-end))
+    (deactivate-mark)
+    (message "Current region added as context."))
+   ;; No region is currently selected, so delete a context under point if there
+   ;; is one.
+   ((gptel-context-at-point)
+    (gptel-remove-context (gptel-context-at-point))
+    (message "Context under point has been removed."))
+   ;; No region is selected and no context is under point.  The default 
behavior
+   ;; is to add the entire buffer as context.
+   (t
+    (gptel--add-region-as-context (current-buffer) (point-min) (point-max))
+    (message "Current buffer added as context."))))
+  
+(defun gptel--make-context-overlay (start end)
+  "Highlight the region from START to END."
+  (let ((overlay (make-overlay start end)))
+    (overlay-put overlay 'face gptel-context-highlight-face)
+    (overlay-put overlay 'gptel-context t)
+    overlay))
+
+(cl-defun gptel--add-region-as-context (buffer region-beginning region-end)
+  "Add region delimited by REGION-BEGINNING, REGION-END in BUFFER as context."
+  ;; Remove existing contexts in the same region, if any.
+  (mapc #'gptel-remove-context
+        (gptel-contexts-in-region buffer region-beginning region-end))
+  (let ((start (make-marker))
+        (end (make-marker)))
+    (set-marker start region-beginning (current-buffer))
+    (set-marker end region-end (current-buffer))
+    ;; Trim the unnecessary parts of the context content.
+    (let* ((content (buffer-substring-no-properties start end))
+           (fat-at-end (progn
+                         (let ((match-pos
+                                (string-match-p (rx (+ (any "\t" "\n" " ")) 
eos)
+                                                content)))
+                           (when match-pos
+                             (- (- end start) match-pos)))))
+           (fat-at-start (progn
+                           (when (string-match (rx bos (+ (any "\t" "\n" " ")))
+                                               content)
+                             (match-end 0)))))
+      (when fat-at-start
+        (set-marker start (+ start fat-at-start)))
+      (when fat-at-end
+        (set-marker end (- end fat-at-end))))
+    (when (= start end)
+      (message "No content in selected region.")
+      (cl-return-from gptel--add-region-to-contexts nil))
+    ;; First, highlight the region.
+    (prog1 (gptel--make-context-overlay start end)
+      (message "Region added to context buffer."))))
+
+;;;###autoload
+(defun gptel-contexts-in-region (buffer start end)
+  "Return the list of context overlays in the given region, if any, in BUFFER.
+START and END signify the region delimiters."
+  (with-current-buffer buffer
+    (cl-remove-if-not #'(lambda (overlay)
+                          (overlay-get overlay 'gptel-context))
+                      (overlays-in start end))))
+
+;;;###autoload
+(defun gptel-context-at-point ()
+  "Return the context overlay at point, if any."
+  (car (overlays-in (point) (point))))
+
+;;;###autoload
+(defun gptel-remove-context (&optional context)
+  "Remove the CONTEXT overlay from the contexts list.
+If CONTEXT is nil, removes the context at point.
+If selection is active, removes all contexts within selection."
+  (interactive)
+  (cond
+   ((overlayp context)
+    (delete-overlay context))
+   ((region-active-p)
+    (let ((contexts (gptel-contexts-in-region (current-buffer)
+                                              (region-beginning)
+                                              (region-end))))
+      (when contexts
+        (cl-loop for ctx in contexts do (delete-overlay ctx)))))
+   (t
+    (let ((ctx (gptel-context-at-point)))
+      (when ctx
+        (delete-overlay ctx))))))
+
+;;;###autoload
+(defun gptel-contexts ()
+  "Get the list of all context overlays in all active buffers."
+  (cl-remove-if-not #'(lambda (ov)
+                        (overlay-get ov 'gptel-context))
+                    (let ((all-overlays '()))
+                      (dolist (buf (buffer-list))
+                        (with-current-buffer buf
+                          (setq all-overlays
+                                (append all-overlays
+                                        (overlays-in (point-min)
+                                                     (point-max))))))
+                      all-overlays)))
+
+;;;###autoload
+(defun gptel-contexts-in-buffer (buffer)
+  "Get the list of all context overlays in BUFFER."
+  (cl-remove-if-not
+   #'(lambda (ov)
+       (overlay-get ov 'gptel-context))
+   (let ((all-overlays '()))
+     (with-current-buffer buffer
+       (setq all-overlays
+             (append all-overlays
+                     (overlays-in (point-min)
+                                  (point-max)))))
+     all-overlays)))
+
+;;;###autoload
+(defun gptel-remove-all-contexts ()
+  "Clear all contexts."
+  (interactive)
+  (mapc #'gptel-remove-context
+        (gptel-contexts)))
+
+;;;###autoload
+(defun gptel-major-mode-md-prog-lang (mode)
+  "Get the Markdown programming language string for the given MODE."
+  (cond
+   ((eq mode 'emacs-lisp-mode) "emacs-lisp")
+   ((eq mode 'lisp-mode) "common-lisp")
+   ((eq mode 'c-mode) "c")
+   ((eq mode 'c++-mode) "c++")
+   ((eq mode 'javascript-mode) "javascript")
+   ((eq mode 'python-mode) "python")
+   ((eq mode 'ruby-mode) "ruby")
+   ((eq mode 'java-mode) "java")
+   ((eq mode 'go-mode) "go")
+   ((eq mode 'rust-mode) "rust")
+   ((eq mode 'haskell-mode) "haskell")
+   ((eq mode 'scala-mode) "scala")
+   ((eq mode 'kotlin-mode) "kotlin")
+   ((eq mode 'typescript-mode) "typescript")
+   ((eq mode 'css-mode) "css")
+   ((eq mode 'html-mode) "html")
+   ((eq mode 'xml-mode) "xml")
+   ((eq mode 'swift-mode) "swift")
+   ((eq mode 'perl-mode) "perl")
+   ((eq mode 'php-mode) "php")
+   ((eq mode 'csharp-mode) "csharp")
+   ((eq mode 'sql-mode) "sql")
+   (t "")))
+
+(defun gptel--region-inline-p (buffer previous-region current-region)
+  "Return non-nil if CURRENT-REGION begins on the line PREVIOUS-REGION ends in.
+This check pertains only to regions in BUFFER.
+
+PREVIOUS-REGION and CURRENT-REGION should be cons cells (START . END) which
+representthe regions' boundaries within BUFFER."
+  (with-current-buffer buffer
+    (let ((prev-line-end (line-number-at-pos (cdr previous-region)))
+          (curr-line-start (line-number-at-pos (car current-region))))
+      (= prev-line-end curr-line-start))))
+
+(defun gptel--regions-continuous-p (buffer previous-region current-region)
+  "Return non-nil if CURRENT-REGION is a continuation of PREVIOUS-REGION.
+Pretains only to regions in BUFFER.
+
+A region is considered a continuation of another if it is only separated by
+newlines and whitespaces.  PREVIOUS-REGION and CURRENT-REGION should be cons
+cells (START . END) representing the boundaries of the regions within BUFFER."
+  (with-current-buffer buffer
+    (let ((gap (buffer-substring-no-properties
+                (cdr previous-region) (car current-region))))
+      (string-match-p
+       (rx bos (* (any "\t" "\n" " ")) eos)
+       gap))))
+
+(defun gptel-buffer-context-string (buffer)
+  "Create a context string from all contexts in BUFFER."
+    (let ((is-top-snippet t)
+          buffer-file
+          previous-region
+          buffer-point-min
+          buffer-point-max
+          prog-lang-tag
+          (contexts (gptel-contexts-in-buffer buffer)))
+      (with-current-buffer buffer
+        (setq buffer-point-min (save-excursion
+                                 (goto-char (point-min))
+                                 (skip-chars-forward " \t\n\r")
+                                 (point))
+              buffer-point-max (save-excursion
+                                 (goto-char (point-max))
+                                 (skip-chars-backward " \t\n\r")
+                                 (point))
+              prog-lang-tag (gptel-major-mode-md-prog-lang
+                             major-mode)))
+      (setq buffer-file
+            ;; Use file path if buffer has one, otherwise use its regular name.
+            (if (buffer-file-name buffer)
+                (format "`%s`"
+                        (buffer-file-name buffer))
+              (format "buffer `%s`"
+                      (buffer-name buffer))))
+      (with-temp-buffer
+        (insert (format "In %s:" buffer-file))
+        (insert "\n\n```" prog-lang-tag "\n")
+        (cl-loop for context in contexts do
+                 (progn
+                   (let* ((start (overlay-start context))
+                          (end (overlay-end context))
+                          (region-inline
+                           ;; Does the current region start on the same line 
the
+                           ;; previous region ends?
+                           (when previous-region
+                             (gptel--region-inline-p buffer
+                                                     previous-region
+                                                     (cons start end))))
+                          (region-continuous
+                           ;; Is the current region a continuation of the
+                           ;; previous region? I.e., is it only separated by
+                           ;; newlines and whitespaces?
+                           (when previous-region
+                             (gptel--regions-continuous-p buffer
+                                                          previous-region
+                                                          (cons start end)))))
+                     (unless (<= start buffer-point-min)
+                       (if region-continuous
+                           ;; If the regions are continuous, insert the
+                           ;; whitespaces that separate them.
+                           (insert-buffer-substring-no-properties
+                            buffer
+                            (cdr previous-region)
+                            start)
+                         ;; Regions are not continuous. Are they on the same
+                         ;; line?
+                         (if region-inline
+                             ;; Region is inline but not continuous, so we
+                             ;; should just insert an ellipsis.
+                             (insert " ... ")
+                           ;; Region is neither inline nor continuous, so just
+                           ;; insert an ellipsis on a new line.
+                           (unless is-top-snippet
+                             (insert "\n"))
+                           (insert "...")))
+                       (let (lineno)
+                         (with-current-buffer buffer
+                           (setq lineno (line-number-at-pos start)))
+                         ;; We do not need to insert a line number indicator on
+                         ;; inline regions.
+                         (unless (or region-inline region-continuous)
+                           (insert (format " (Line %d)" lineno)))))
+                     (when (or (and (not region-inline)
+                                    (not region-continuous)
+                                    (not is-top-snippet))
+                               is-top-snippet)
+                       (insert "\n"))
+                     (if is-top-snippet
+                         (setq is-top-snippet nil))
+                     (let (substring)
+                       (with-current-buffer buffer
+                         (setq substring (buffer-substring-no-properties
+                                          start end)))
+                       ;; This text property will allow us to know what overlay
+                       ;; is associated to which context.
+                       (put-text-property 0 (length substring)
+                                          'gptel-context-overlay
+                                          context substring)
+                       (insert substring))
+                     (setq previous-region (cons start end)))))
+        (unless (>= (overlay-end (car (last contexts))) buffer-point-max)
+          (insert "\n..."))
+        (insert "\n```")
+        (buffer-substring (point-min) (point-max)))))
+
+;;;###autoload
+(defun gptel-context-string ()
+  "Return the context string of all aggregated contexts."
+  (string-trim-right
+   (cl-loop for buffer in
+            (delete-dups (mapcar #'overlay-buffer (gptel-contexts)))
+            concat (concat (gptel-buffer-context-string buffer) "\n\n"))))
+
+(provide 'gptel-contexter)
+;;; gptel-contexter.el ends here.
diff --git a/gptel-transient.el b/gptel-transient.el
index ec723904f5..30db75950d 100644
--- a/gptel-transient.el
+++ b/gptel-transient.el
@@ -28,6 +28,7 @@
 (require 'cl-lib)
 (require 'gptel)
 (require 'transient)
+(require 'gptel-contexter)
 
 (declare-function ediff-regions-internal "ediff")
 (declare-function ediff-make-cloned-buffer "ediff-utils")
@@ -467,6 +468,76 @@ Customize `gptel-directives' for task-specific prompts."
 
 ;; * Transient Infixes
 
+;; ** Infixes for context aggregation
+
+(defclass gptel-keyword-variable (transient-lisp-variable)
+  ((choices :initarg :choices)
+   (always-read :initform t)
+   (set-value :initarg :set-value :initform #'set))
+  "Class for handling variables with keyword choices.")
+
+(cl-defmethod transient-format-value ((obj gptel-keyword-variable))
+  (let ((keyword-value (oref obj value))
+        (choices (oref obj choices)))
+    (propertize (cdr (assoc keyword-value choices)) 'face 'transient-value)))
+
+(cl-defmethod transient-infix-set ((obj gptel-keyword-variable) value)
+  (let ((keyword (car (rassoc value (oref obj choices)))))
+    (funcall (oref obj set-value)
+             (oref obj variable)
+             (oset obj value keyword))))
+
+(defun gptel--keyword-reader (prompt choices)
+  (let* ((display-choices (mapcar #'cdr choices))
+         (selected-string (completing-read prompt display-choices nil t)))
+    selected-string))
+
+(transient-define-infix gptel--infix-context-destination ()
+  "Describe target destination for context injection."
+  :description "Context destination"
+  :class 'gptel-keyword-variable
+  :variable 'gptel-context-injection-destination
+  :key "-xd"
+  :choices '((:nowhere . "nowhere")
+             (:before-system-message . "before system message")
+             (:after-system-message . "after system message")
+             (:before-user-prompt . "before user prompt")
+             (:after-user-prompt . "after user prompt"))
+  :reader (lambda (prompt &rest _)
+            (gptel--keyword-reader
+             prompt 
+             '((:nowhere . "nowhere")
+               (:before-system-message . "before system message")
+               (:after-system-message . "after system message")
+               (:before-user-prompt . "before user prompt")
+               (:after-user-prompt . "after user prompt")))))
+
+(defclass gptel-boolean-variable (transient-lisp-variable)
+  ((always-read :initform t)
+   (set-value :initarg :set-value :initform #'set))
+  "Class for handling boolean variables.")
+
+(cl-defmethod transient-format-value ((obj gptel-boolean-variable))
+  (let ((value (oref obj value)))
+    (propertize (if value "yes" "no") 'face 'transient-value)))
+
+(cl-defmethod transient-infix-set ((obj gptel-boolean-variable) value)
+  (funcall (oref obj set-value)
+           (oref obj variable)
+           (oset obj value (equal value "yes"))))
+
+(defun gptel--boolean-reader (prompt _ history)
+  (let* ((choice (completing-read prompt '("yes" "no") nil t nil history)))
+    choice))
+
+(transient-define-infix gptel--infix-use-context-in-chat ()
+  "Determine if context should be passed to the LLM during the chat."
+  :description "Use in chat"
+  :class 'gptel-boolean-variable
+  :variable 'gptel-use-context-in-chat
+  :key "-xc"
+  :reader 'gptel--boolean-reader)
+
 ;; ** Infixes for model parameters
 
 (transient-define-infix gptel--infix-variable-scope ()
@@ -881,6 +952,299 @@ When LOCAL is non-nil, set the system message only in the 
current buffer."
                          (funcall quit-to-menu)))
         (local-set-key (kbd "C-c C-k") quit-to-menu)))))
 
+;; ** Suffix for displaying and removing context
+
+(defun gptel--context-edge-point (edge-type direction &optional inclusive)
+  "Find context edge point of EDGE-TYPE from current point.
+EDGE-TYPE is either :start or :end.
+DIRECTION is either :next or :previous.
+If INCLUSIVE is non-nil, return the current point if it is on an edge."
+  (let* ((point nil)
+         (get-edge-point #'(lambda (direction)
+                             (if (and inclusive
+                                      (when (get-text-property (point) 
'gptel-context-overlay)
+                                        (if (eq edge-type :start)
+                                            (when (and (/= (point) (point-min))
+                                                       (not (get-text-property
+                                                             (1- (point))
+                                                             
'gptel-context-overlay)))
+                                              t)
+                                          (when (and (/= (point) (point-max))
+                                                     (not (get-text-property
+                                                           (1+ (point))
+                                                           
'gptel-context-overlay)))
+                                            t))))
+                                 (point)
+                               (if (eq direction :next)
+                                   (setq point (next-single-property-change
+                                                (point)
+                                                'gptel-context-overlay nil 
nil))
+                                 (setq point (previous-single-property-change
+                                                (point)
+                                                'gptel-context-overlay nil 
nil)))))))
+    (save-excursion
+      (if (eq edge-type :end)
+          (progn
+            (funcall get-edge-point direction)
+            (when point
+              (goto-char point)
+              (if (get-text-property (1+ point) 'gptel-context-overlay)
+                  ;; This is actually a starting edge, not an ending edge.
+                  (funcall get-edge-point direction)
+                point)))
+        (funcall get-edge-point direction)
+        (when point
+          (goto-char point)
+          (if (get-text-property (1- point) 'gptel-context-overlay)
+              ;; This is actually an ending edge, not a starting edge.
+              (funcall get-edge-point direction)))))
+    ;; Handle some edge cases (pun unintended).
+    (unless point
+      (if (eq edge-type :end)
+          (when (get-text-property (max (point-min) (1- (point))) 
'gptel-context-overlay)
+            (setq point (point)))
+        (when (get-text-property (min (point-max) (1+ (point))) 
'gptel-context-overlay)
+            (setq point (point)))))
+    point))
+
+(let* ((highlight-start nil)
+       (highlight-end nil)
+       (highlight-overlay nil)
+       (moved-backwards nil)) ; This is used for some deletion navigation QoL.
+  
+  (transient-define-suffix gptel--suffix-context-buffer ()
+    "Display all contexts from all buffers & files."
+    :transient 'transient--do-exit
+    :key "-xb"
+    :description (lambda ()
+                   (let* ((contexts (gptel-contexts))
+                          (buffer-count (length (delete-dups (mapcar 
#'overlay-buffer contexts)))))
+                     (concat "Display context buffer "
+                             (propertize
+                              (format "%d context%s in %d buffer%s"
+                                      (length contexts)
+                                      (if (/= (length contexts) 1) "s" "")
+                                      buffer-count
+                                      (if (/= buffer-count 1) "s" ""))
+                              'face 'transient-value))))
+    (interactive)
+    (let ((orig-buf (current-buffer)))
+      (with-current-buffer (get-buffer-create "*gptel-context*")
+        (read-only-mode 1)
+        (setq highlight-start nil
+              highlight-end nil
+              highlight-overlay nil)
+        (let ((inhibit-read-only t))
+          (erase-buffer)
+          (setq header-line-format
+                (concat
+                 "Mark/unmark deletion with "
+                 (propertize "d" 'face 'help-key-binding)
+                 ", jump to next/previous with "
+                 (propertize "n" 'face 'help-key-binding)
+                 "/"
+                 (propertize "p" 'face 'help-key-binding)
+                 ", respectively. "
+                 (propertize "C-c C-c" 'face 'help-key-binding)
+                 " to apply, or "
+                 (propertize "C-c C-k" 'face 'help-key-binding)
+                 " to abort."))
+          (save-excursion
+            (let ((contexts (gptel-contexts)))
+              (if (> (length contexts) 0)
+                  (insert (gptel-context-string))
+                (insert "There are no active contexts in any buffer.")))))
+        (display-buffer (current-buffer)
+                        `((display-buffer-below-selected)
+                          (body-function . ,#'select-window)
+                          (window-height . ,#'fit-window-to-buffer)))
+        ;; Add hook to change the highlight whenever the point has moved beyond
+        ;; that of the current highlight.
+        (add-hook
+         'post-command-hook
+         #'(lambda ()
+             ;; Only update if point moved outside the current region.
+             (unless (and highlight-start highlight-end
+                          (>= (point) highlight-start)
+                          (<= (point) highlight-end))
+               ;; Remove the old region.
+               (when highlight-overlay (delete-overlay highlight-overlay))
+               (setq highlight-end nil
+                     highlight-start nil)
+               ;; Find new region to highlight.
+               (let* ((point-is-within-context
+                       (get-text-property (point) 'gptel-context-overlay))
+                      (start (previous-single-property-change
+                              (point)
+                              'gptel-context-overlay
+                              nil
+                              nil))
+                      (end (next-single-property-change
+                            (point)
+                            'gptel-context-overlay
+                            nil
+                            nil)))
+                 ;; Handle the edge cases where the point is located at the 
ends
+                 ;; of the context.
+                 (when (or (not start)
+                           (not (get-text-property (1+ start)
+                                                   'gptel-context-overlay)))
+                   (setq start (point)))
+                 (when (and start end (<= start (point) end)
+                            point-is-within-context)
+                   (setq highlight-start start)
+                   (setq highlight-end (1- end))
+                   ;; Create new overlay for highlighting.
+                   (setq highlight-overlay (make-overlay start end))
+                   (overlay-put highlight-overlay 'face 'highlight)
+                   (overlay-put highlight-overlay 'priority 1)
+                   (overlay-put highlight-overlay 'gptel-context-highlight 
t)))))
+         nil t)
+        (let ((quit-to-menu
+               (lambda ()
+                 (interactive)
+                 (local-unset-key (kbd "d"))
+                 (local-unset-key (kbd "n"))
+                 (local-unset-key (kbd "p"))
+                 (local-unset-key (kbd "C-c C-c"))
+                 (local-unset-key (kbd "C-c C-k"))
+                 (quit-window)
+                 (display-buffer
+                  orig-buf
+                  `((display-buffer-reuse-window
+                     display-buffer-use-some-window)
+                    (body-function . ,#'select-window)))
+                 (call-interactively #'gptel-menu)))
+              (forward-movement-func
+               #'(lambda ()
+                   (interactive)
+                   (let ((next-start (gptel--context-edge-point :start :next)))
+                     (when next-start
+                       (setq moved-backwards nil)
+                       (goto-char next-start)))))
+              (backward-movement-func
+               #'(lambda ()
+                   (interactive)
+                   (let ((previous-end (gptel--context-edge-point :end 
:previous)))
+                     (when (and previous-end (/= previous-end (point)))
+                       (setq moved-backwards t)
+                       (goto-char (1- previous-end)))))))
+          (local-set-key (kbd "n") forward-movement-func)
+          (local-set-key (kbd "p") backward-movement-func)
+          (local-set-key
+           (kbd "d") ; Marking overlays for deletion
+           #'(lambda ()
+               (interactive)
+               (if (not (region-active-p)) ; Separate functiaonlity with just 
points vs. regions.
+                   (progn
+                     (let ((overlays (overlays-at (point)))
+                           (deletion-overlay-found nil)
+                           (highlighting-overlay nil)
+                           (something-marked-or-unmarked nil))
+                       ;; Loop through all overlays at point to check for 
deletion mark or
+                       ;; highlight.
+                       (dolist (overlay overlays)
+                         (cond
+                          ((overlay-get overlay 'gptel-context-deletion-mark)
+                           ;; If deletion mark is found, delete the overlay 
and set flag to true.
+                           (delete-overlay overlay)
+                           (setq something-marked-or-unmarked t)
+                           (setq deletion-overlay-found t))
+                          ((overlay-get overlay 'gptel-context-highlight)
+                           (setq highlighting-overlay overlay))))
+                       (when (and highlighting-overlay
+                                  (not deletion-overlay-found)
+                                  (overlay-get highlighting-overlay 
'gptel-context-highlight))
+                         (let* ((start (overlay-start highlighting-overlay))
+                                (end (overlay-end highlighting-overlay))
+                                (new-overlay (make-overlay start end)))
+                           ;; We want to have 0 priority so that the 
highlighting overlay takes
+                           ;; precedence.
+                           (setq something-marked-or-unmarked t)
+                           (overlay-put new-overlay 'priority 0)
+                           (overlay-put new-overlay 'face 
'diff-indicator-removed)
+                           (overlay-put new-overlay 
'gptel-context-deletion-mark t)))
+                       (when something-marked-or-unmarked
+                         (if moved-backwards
+                             (progn
+                               (let ((point (point)))
+                                 (funcall backward-movement-func)
+                                 (when (eq point (point))
+                                   ;; We haven't moved.  Disregard previous 
movement and just go
+                                   ;; forwards.
+                                   (setq moved-backwards nil)
+                                   (funcall forward-movement-func))))
+                           (funcall forward-movement-func)))))
+                 ;; We have a region selected, so we must iterate all the 
overlays in it to do the
+                 ;; same as we have done above.
+                 (let ((marking-action :mark-all) ; :mark-all, :unmark-all
+                       (context-region-and-mark '())
+                       (start (region-beginning))
+                       (end (region-end))
+                       (unmarked-context-found nil)
+                       (highlight-overlay-region-at-point
+                        #'(lambda ()
+                            ;; We can't get the overlay, because the hook 
isn't triggered, so the
+                            ;; highlighting overlay won't work when we use 
`goto-char'.
+                            (when (get-text-property (point) 
'gptel-context-overlay)
+                              (cons (gptel--context-edge-point :start 
:previous t)
+                                    (gptel--context-edge-point :end :next 
t)))))
+                       (deletion-overlay-at-point
+                        #'(lambda ()
+                            (car (cl-loop
+                                  for ov in (overlays-at (point))
+                                  when (overlay-get ov 
'gptel-context-deletion-mark)
+                                  collect ov)))))
+                   (deactivate-mark)
+                   ;; We want to collect the context regions and see if they 
have deletion marks to
+                   ;; determine what we want to do.
+                   (save-excursion
+                     (goto-char start)
+                     (cl-loop for previous-point = start then (point)
+                              do (progn
+                                   (let ((hov-region (funcall 
highlight-overlay-region-at-point))
+                                         (deletion-ov nil))
+                                     (when hov-region
+                                       (setq deletion-ov (funcall 
deletion-overlay-at-point))
+                                       (push (cons hov-region
+                                                   deletion-ov)
+                                             context-region-and-mark)
+                                       (unless deletion-ov
+                                         (setq unmarked-context-found t)))
+                                     (funcall forward-movement-func)))
+                              until (or (= previous-point (point))
+                                        (> (point) end))))
+                   (unless unmarked-context-found
+                     (setq marking-action :unmark-all))
+                   (cl-loop for (hov-region . dov) in context-region-and-mark 
do
+                            (if (eq marking-action :mark-all)
+                                (unless dov ; Do not make a duplicate deletion 
overlay.
+                                  (let* ((start (car hov-region))
+                                         (end (cdr hov-region))
+                                         (new-overlay (make-overlay start 
end)))
+                                    (overlay-put new-overlay 'priority 0)
+                                    (overlay-put new-overlay 'face 
'diff-indicator-removed)
+                                    (overlay-put new-overlay 
'gptel-context-deletion-mark t)))
+                              ;; marking-action is :unmark-all.
+                              (delete-overlay dov)))))))
+          (local-set-key (kbd "C-c C-c")
+                         #'(lambda ()
+                             (interactive)
+                             ;; Delete all the context overlays that have been 
marked for deletion.
+                             (cl-loop for dov in
+                                      (cl-loop for ov in (overlays-in 
(point-min) (point-max))
+                                               when
+                                               (and (overlay-get ov 
'gptel-context-deletion-mark)
+                                                    ;; Ignore zero-length 
overlays.  Not sure why
+                                                    ;; these appear at the 
start of the buffer.
+                                                    (/= (overlay-start ov) 
(overlay-end ov)))
+                                               collect ov)
+                                      do (delete-overlay
+                                          (get-text-property (overlay-start 
dov)
+                                                             
'gptel-context-overlay)))
+                             (funcall quit-to-menu)))
+          (local-set-key (kbd "C-c C-k") quit-to-menu))))))
+
 ;; ** Suffixes for rewriting/refactoring
 
 (transient-define-suffix gptel--suffix-rewrite ()
diff --git a/gptel.el b/gptel.el
index 5f217e0abc..e72db51ccf 100644
--- a/gptel.el
+++ b/gptel.el
@@ -1378,6 +1378,32 @@ context for the ediff session."
   (interactive "p")
   (gptel--previous-variant (- arg)))
 
+(defun gptel-clean-up-llm-code (buffer beg end)
+  "Clean up LLM response between BEG & END in BUFFER.
+
+Removes any markup formatting and indents the code within the parameters of the
+current buffer."
+  (with-current-buffer buffer
+    (save-excursion
+      (let* ((res-beg beg)
+             (res-end end)
+             (contents nil))
+        (setq contents (buffer-substring-no-properties res-beg
+                                                       res-end))
+        (setq contents (replace-regexp-in-string
+                        "^\\(```.*\n\\)\\|\n\\(```.*\\)$"
+                        ""
+                        contents))
+        (delete-region res-beg res-end)
+        (goto-char res-beg)
+        (insert contents)
+        (setq res-end (point))
+        ;; Indent the code to match the buffer indentation if it's messed up.
+        (unless (eq indent-line-function #'indent-relative)
+          (indent-region res-beg res-end))
+        (pulse-momentary-highlight-region res-beg res-end)
+        (setq res-beg (next-single-property-change res-beg 'gptel))))))
+
 (provide 'gptel)
 ;;; gptel.el ends here
 


Reply via email to