Skip to content

Instantly share code, notes, and snippets.

@mwolson
Last active January 24, 2025 08:32
Show Gist options
  • Save mwolson/82672c551299b457848a3535ccb6c4ea to your computer and use it in GitHub Desktop.
Save mwolson/82672c551299b457848a3535ccb6c4ea to your computer and use it in GitHub Desktop.
gptel-manual-complete.el

gptel-manual-complete.el

This is an example of how the existing gptel-rewrite.el file can be used to perform completion on an entire function, replacing what's already written so far in that function.

Setup

To use:

  • Install gptel: https://github.com/karthink/gptel , configure it, and provide the appropriate API keys
  • Download the gptel-manual-complete.el file in this gist (below) to a location like ~/gptel-manual-complete/gptel-manual-complete.el
  • Add the following to your Emacs configuration (typically ~/.emacs.d/init.el or similar):
    (add-to-list 'load-path (expand-file-name "~/gptel-manual-completion"))
    (autoload #'gptel-manual-complete "gptel-manual-complete" t)
  • Now choose which key you'd like to bind it to. I typically add something like this to my Emacs config:
    (defvar my-xref-map
      (let ((map (make-sparse-keymap)))
        (define-key map (kbd "c") #'gptel-manual-complete)
        (define-key map (kbd ".") #'xref-find-definitions)
        (define-key map (kbd ",") #'xref-go-back)
        (define-key map (kbd "/") #'xref-find-references)
        map)
      "My key customizations for AI and xref.")
    
    (global-set-key (kbd "C-c .") my-xref-map)
  • Restart Emacs

Usage

If you've used the above keybinds, they work like this (the only with AI is the first one):

  • C-c . c to complete the code at point using Claude AI; if you have a comment near the end, that will better inform the completion
  • C-c . . to visit the definition of the thing at point
  • C-c . , to return to the original point after visiting something
  • C-c . / to find references to the thing at point

Example

When I write this code in a sample.el file:

(defun my-code ()
  "AI should not modify this."
  (message "Sample 1"))

(defun my-hello
;; print a welcoming message in a window off to the right
)

(defun my-other-code ()
  "AI should not modify this either."
  (message "Sample 2"))

Move the cursor into the body of my-hello and hit C-c . c then gptel will rewrite that my-hello function to something like this, without touching the other functions or deleting lines around it (results may vary, I used Claude 3.5 Sonnet in this example):

(defun my-hello ()
  "Print a welcoming message in a window off to the right."
  (let ((buf (get-buffer-create "*Hello*")))
    (with-current-buffer buf
      (erase-buffer)
      (insert "Welcome to Emacs!\n\nHave a productive session."))
    (display-buffer buf
                    '((display-buffer-reuse-window
                       display-buffer-in-side-window)
                      (side . right)
                      (window-width . 40)))))

From here, you can use the standard gptel-rewrite keys like C-c C-a on that code to accept it and remove the overlay on it.

Note that the function must have balanced parentheses, otherwise the code will throw an error. This is to make it easier to locate the beginning and end of the function to send to gptel's context.

Inspiration

After adding a function to gptel's context, I was using gptel-rewrite and accidentally hit Enter twice. This resulted in just the basic "Rewrite: " text being sent, and to my surprise that was very effective at having Claude fix the problem I was going to ask about.

I decided to see if Claude could also do code completions this way, with a very terse kind of prompt on top of the standard gptel-rewrite prompt, and it turns out that it can!

Notes

  • This is intended to be more of a tech demo than a final project; it piggybacks on top of gptel-rewrite instead of doing things a more idiomatic way. I'd love for this to be improved upon, ideally with a solution that's part of gptel itself.
  • I've only tested this with Claude.
  • For automatically identifying the entire current function to complete, you may have the best luck with either Emacs Lisp or files with major modes that have a tree-sitter grammar installed, as otherwise we have to guess. In general it should err on the side of sending too little rather than too much.
  • My Emacs setup is available at https://github.com/mwolson/emacs-shared which has this and other features
    • Note that the install doc might take a while to get through, and may have opinionated settings
    • The additional AI features which have more bindings on C-c . than in the above example are described here
  • karthink and other gptel contributors may use the code in this gist freely and reassign copyright to themselves as need be if they would like.
;;; gptel-manual-complete.el --- Manual completion for gptel -*- lexical-binding: t; -*-
;; Copyright (C) 2025 Michael Olson
;; Copyright (C) 2024 Karthik Chikmagalur
;; Author: Michael Olson <[email protected]>
;; Keywords: hypermedia, convenience, tools
;; 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:
;;
;;; Code:
(require 'gptel)
(require 'gptel-rewrite)
(defvar gptel-manual-complete-directive "Complete at end: ")
(defun gptel-manual-complete--mark-function-default (&optional steps)
(let ((pt-min (point))
(pt-mid (point))
(pt-max (point)))
(save-mark-and-excursion
(ignore-errors
(mark-defun steps)
(setq pt-min (region-beginning)
pt-max (region-end))))
(save-mark-and-excursion
(mark-paragraph steps)
(when (<= (region-beginning) pt-min)
(when (save-excursion
(goto-char pt-mid)
(beginning-of-line)
(looking-at-p "[[:space:]]*$"))
(forward-paragraph 1))
(setq pt-min (region-beginning)
pt-max (max pt-max (region-end)))))
(set-mark pt-min)
(goto-char pt-max)))
(defun gptel-manual-complete--mark-function-treesit (&optional steps)
(treesit-end-of-defun)
(let ((pt-max (point)))
(treesit-beginning-of-defun)
(setq steps (1- (- 0 (or steps 0))))
(while (> steps 0)
(treesit-beginning-of-defun)
(cl-decf steps))
(set-mark (point))
(goto-char pt-max)))
(defun gptel-manual-complete--mark-function (&optional steps)
"Put mark at end of this function, point at beginning.
If STEPS is negative, mark `- arg - 1` extra functions backward.
The behavior for when STEPS is positive is not currently well-defined."
(interactive)
(let ((pt-min (point))
(pt-max nil))
(when (null steps) (setq steps -1))
(when (treesit-parser-list)
(save-mark-and-excursion
(gptel-manual-complete--mark-function-treesit steps)
(setq pt-min (region-beginning)
pt-max (region-end))))
(gptel-manual-complete--mark-function-default steps)
(when (< (region-beginning) pt-min)
(setq pt-min (region-beginning)
pt-max (region-end)))
(goto-char pt-min)
(while (and (looking-at-p "[[:space:]\r\n]")
(< (point) pt-max))
(forward-char))
(setq pt-min (point))
(goto-char (1- pt-max))
(push-mark pt-min nil t)))
;;;###autoload (autoload 'gptel-manual-complete "gptel-manual-complete" nil t)
(defun gptel-manual-complete ()
"Complete using an LLM.
Either the last function or the current region will be used for context."
(interactive)
(gptel-manual-complete--mark-function)
(gptel-manual-complete-send))
(defun gptel-manual-complete-send ()
"Complete using an LLM."
(let* ((nosystem (gptel--model-capable-p 'nosystem))
;; Try to send context with system message
(gptel-use-context
(and gptel-use-context (if nosystem 'user 'system)))
(prompt (list (or (get-char-property (point) 'gptel-rewrite)
(buffer-substring-no-properties (region-beginning) (region-end)))
"What is the required change?"
gptel-manual-complete-directive))
(buffer (current-buffer)))
(deactivate-mark)
(when nosystem
(setcar prompt (concat (car-safe (gptel--parse-directive
gptel--rewrite-directive 'raw))
"\n\n" (car prompt))))
(gptel-request prompt
:dry-run nil
:system gptel--rewrite-directive
:stream gptel-stream
:context
(let ((ov (or (cdr-safe (get-char-property-and-overlay (point) 'gptel-rewrite))
(make-overlay (region-beginning) (region-end) nil t))))
(overlay-put ov 'category 'gptel)
(overlay-put ov 'evaporate t)
(cons ov (generate-new-buffer "*gptel-manual-complete*")))
:callback `(lambda (&rest args)
(apply #'gptel--rewrite-callback args)
(with-current-buffer ,buffer
(backward-char))))))
(provide 'gptel-manual-complete)
;;; gptel-manual-complete.el ends here
;; Local Variables:
;; outline-regexp: "^;; \\*+"
;; End:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment