diff --git a/gptel-contexter.el b/gptel-contexter.el index 154dade..2aa9d84 100644 --- a/gptel-contexter.el +++ b/gptel-contexter.el @@ -31,23 +31,17 @@ (require 'cl-lib) -(defvar gptel--context-buffer nil) -(defvar gptel--contexts-alist nil - "An association list from buffers to a list of regions.") -(defvar gptel--current-highlight-region nil) -(defvar gptel--context-buffer-point nil) - ;; We need a minor mode for a custom keymap, so that we may be able to remove ;; contexts directly from the contexts buffer. -(define-minor-mode context-mode +(define-minor-mode gptel-context-mode "A minor mode for working with context." - :lighter " Context" + :lighter " GPTel Context" :keymap (let ((map (make-sparse-keymap))) (define-key map (kbd "c") - 'gptel-remove-context-at-point-from-context-buffer) + 'gptel-remove-context-at-point) map)) -(defcustom gptel-contexter-selection-face 'secondary-selection +(defcustom gptel-context-highlight-face 'header-line "Face to use to highlight selected context in the buffers." :group 'gptel :type 'symbol) @@ -56,24 +50,41 @@ ;;; ------------------------------ FUNCTIONS ------------------------------- ;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defun gptel--highlight-region (start end) +;;;###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 in `dired-mode', adds selected files as 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)) + ;; No region is currently selected, so delete a context under point if there + ;; is one. + ((gptel-context-at-point) + (gptel-delete-context (gptel-context-at-point)) + (message "Context under point has been deleted.")))) + +(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-contexter-selection-face) - (overlay-put overlay 'contexter t))) + (overlay-put overlay 'face gptel-context-highlight-face) + (overlay-put overlay 'gptel-context t) + overlay)) -(defun gptel--unhighlight-region (start end) - "Remove highlighting from the region between START and END." - (dolist (overlay (overlays-in start end)) - (when (and (overlay-get overlay 'contexter) - (overlay-get overlay 'face)) - (delete-overlay overlay)))) - -(cl-defun gptel--add-region-to-contexts () - "Add the selected region to te contexts. -Order of the contexts in a buffer is determined by their order in the buffer." - (unless (use-region-p) - (error "No region selected")) +(cl-defun gptel--add-region-as-context (buffer start end) + "Add the region delimited by START and END in BUFFER as context." + ;; Remove existing contexts in the same region, if any. + (mapc #'gptel-delete-context + (gptel-contexts-in-region buffer start end)) (let ((start (make-marker)) (end (make-marker))) (set-marker start (region-beginning) (current-buffer)) @@ -98,163 +109,51 @@ Order of the contexts in a buffer is determined by their order in the buffer." (message "No content in selected region.") (cl-return-from gptel--add-region-to-contexts nil)) ;; First, highlight the region. - (gptel--highlight-region start end) - ;; Then, add the region to `gptel--contexts-alist', associating it with - ;; the current buffer. - (let ((existing-entry (assoc (current-buffer) gptel--contexts-alist))) - (if (not existing-entry) - (add-to-list 'gptel--contexts-alist - (cons (current-buffer) (list (list start end)))) - (let ((regions (cdr existing-entry)) - (new-region (list (list start end)))) - (setcdr existing-entry - (cl-sort (append regions new-region) - (lambda (a b) - (or (<= (car a) (car b))))))))) - (message "Region added to context buffer.") - t)) + (prog1 (gptel--make-context-overlay start end) + (message "Region added to context buffer.")))) ;;;###autoload -(defun gptel-context-in-region (buffer start end) - "Return the context in the given region, if any, in BUFFER. -START and END signify the region delimiters. -A context in this function is a cons cell with the buffer as the CAR and the -region tuple as the CDR. The tuple is a list, not a cons cell." - (interactive) - (let (context regions) - (setq context (assoc buffer gptel--contexts-alist)) - (when context - (cl-loop for (ctx-start ctx-end) in (cdr context) - ;; If the current context range overlaps with the argument range. - when (or (<= ctx-start start end ctx-end) - (<= ctx-start start ctx-end end) - (<= start ctx-start end ctx-end) - (<= start ctx-start ctx-end end)) - do (push (list ctx-start ctx-end) regions))) - (if (not (zerop (length regions))) - (setq context (cons buffer regions)) - (setq context nil)) - context)) +(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 at point, if any. -A context in this function is a cons cell with the buffer as the CAR and the -region tuple as the CDR. The tuple is a list, not a cons cell." + "Return the context overlay at point, if any." + (car (overlays-in (point) (point)))) + +;;;###autoload +(defun gptel-delete-context (context) + "Remove the CONTEXT overlay from the contexts list." + (delete-overlay context)) + +;;;###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-delete-all-contexts () + "Clear all contexts." (interactive) - (let ((point (point)) - (buffer (current-buffer)) - context - regions '()) - (setq context (assoc buffer gptel--contexts-alist)) - (when context - (cl-loop for (start end) in (cdr context) - when (and (>= point start) (<= point end)) - do (push (list start end) regions))) - (if (not (zerop (length regions))) - (setq context (cons buffer regions)) - nil))) + (mapc #'gptel-delete-context + (gptel-contexts))) ;;;###autoload -(defun gptel-remove-context (context &optional refresh-context-buffer) - "Remove the CONTEXT snippet and unhighlight its region. -If REFRESH-CONTEXT-BUFFER is set to t, then also refresh the context buffer." - (let* ((buffer (car context)) - (regions (cdr context))) - ;; Unhighlight each region. - (with-current-buffer buffer - (cl-loop for (start end) in regions - do (gptel--unhighlight-region start end))) - ;; Remove regions from the list and clean up if empty. - (setq gptel--contexts-alist - (delq nil - (mapcar (lambda (item) - (if (eq (car item) buffer) - (let ((new-value - (seq-difference (cdr item) regions))) - (unless (seq-empty-p new-value) - (cons (car item) new-value))) - item)) - gptel--contexts-alist)))) - (when refresh-context-buffer - (gptel--refresh-context-buffer))) - -;;;###autoload -(cl-defun gptel-pop-or-push-context () - "Pop or push text into the context buffer depending on selection state. - -If a region is selected, push the region as context. -If no region is selected, try to pop the context at the point. -If a region is selected but it contains contexts, pop all contexts within it and -add the region as a single context.. - -Popping context has no other meaning other than erasing it from the context -buffer." - (interactive) - (let ((context - (if (use-region-p) - (gptel-context-in-region (current-buffer) - (region-beginning) - (region-end)) - (gptel-context-at-point)))) - (when context - (gptel-remove-context context)) - (when (use-region-p) - (unless (gptel--add-region-to-contexts) - (cl-return-from gptel-pop-or-push-context)) - (deactivate-mark))) - (gptel--refresh-context-buffer)) - -;;;###autoload -(defun gptel-remove-all-contexts () - "Clear all saved context regions." - (interactive) - (mapc #'gptel-remove-context gptel--contexts-alist) - (gptel--refresh-context-buffer)) - -(defun gptel--cleanup-killed-buffer () - "Remove contexts if their buffer was killed." - (let ((context (assoc (current-buffer) gptel--contexts-alist))) - (when context - (setq gptel--contexts-alist (assoc-delete-all (current-buffer) - gptel--contexts-alist)) - (gptel--refresh-context-buffer)))) - -;; We don't care about the hook parameters. -(defun gptel--cleanup-degenerate-contexts (_ _ _) - "Clean up contexts by degenerate regions." - (let ((context (assoc (current-buffer) gptel--contexts-alist))) - (when context - (setcdr context - (cl-remove-if (lambda (region) - (= (cl-first region) (cl-second region))) - (cdr context))) - (when (zerop (length (cdr context))) ; All regions were removed! - (setq gptel--contexts-alist - (assoc-delete-all (current-buffer) gptel--contexts-alist))) - (gptel--refresh-context-buffer)))) - -(defun gptel--sync-context-buffer (start end _) - "See if the markers in the buffer have been changed between START and END. -If they have, update the context buffer." - (when (gptel-context-in-region (current-buffer) start end) - (gptel--refresh-context-buffer))) - -(defun gptel--ensure-context-buffer-exists () - "Make sure the context buffer exists. Create it if it does not." - (with-current-buffer (get-buffer-create "*Context*") ; Create, if nonexistant. - (setq gptel--context-buffer (current-buffer)) - ;; Ensure the minor mode context-mode is enabled in this buffer. - (unless (bound-and-true-p context-mode) - (context-mode 1)) - (read-only-mode 1))) ; Set read-only mode. - -(defun gptel--sort-pairs-by-ascending-order (regions) - "Sort the pairs in REGIONS by ascending order." - (sort regions (lambda (a b) (< (car a) (car b))))) - -;;;###autoload -(defun gptel--major-mode-md-prog-lang (mode) +(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") @@ -377,7 +276,7 @@ REGIONS is a list of pairs of (start, end) lists." (goto-char (point-max)) (skip-chars-backward " \t\n\r") (point)) - prog-lang-tag (gptel--major-mode-md-prog-lang + prog-lang-tag (gptel-major-mode-md-prog-lang major-mode))) (insert "\n\n```" prog-lang-tag "\n") (cl-loop for (start end) in regions do @@ -521,7 +420,7 @@ In essence, just highlights the context where the point is in the buffer." (gptel--unhighlight-region current-start current-end) (setq gptel--current-highlight-region nil)) (when context - (gptel--highlight-region start end) + (gptel--make-context-region start end) (setq gptel--current-highlight-region (list start end))))) (cl-defun gptel--monitor-context-buffer-point-change () @@ -536,27 +435,10 @@ Used mainly for selecting contexts when the point has moved." ;;;###autoload (defun gptel-context-string () - "Return the contents of the context buffer." + "Return the string of all the contexts." (gptel--ensure-context-buffer-exists) (with-current-buffer gptel--context-buffer (buffer-substring-no-properties (point-min) (point-max)))) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;;; -------------------------------- HOOKS --------------------------------- ;;; -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(add-hook 'kill-buffer-hook #'gptel--cleanup-killed-buffer) -(add-hook 'after-change-functions - #'gptel--cleanup-degenerate-contexts - #'gptel--sync-context-buffer) -(add-hook 'post-command-hook #'gptel--monitor-context-buffer-point-change) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;;; ---------------------------- INITIALIZATION ---------------------------- ;;; -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -;; Just make sure an empty buffer exists. -(gptel--refresh-context-buffer) - -(provide 'contexter) +(provide 'gptel-contexter) ;;; gptel-contexter.el ends here.