Skip to content

Instantly share code, notes, and snippets.

@mbrock
Last active May 30, 2024 07:03
Show Gist options
  • Save mbrock/3d9ffb70dd1938d767d7fcee95f61c69 to your computer and use it in GitHub Desktop.
Save mbrock/3d9ffb70dd1938d767d7fcee95f61c69 to your computer and use it in GitHub Desktop.
hole mode for emacs
;;; hole-mode.el --- fill holes with GPT-4 -*- lexical-binding: t -*-
;; Author: Mikael Brockman <mikael@brockman.se>
;; Version: 1.0
;;; Commentary:
;; This package provides a minor mode for filling holes in code with AI.
;;
;; It is designed to work with the `llm' command-line tool, which is a
;; thin wrapper around OpenAI's GPT-4 API.
;;
;; Put holes in your code like this:
;;
;; (defun double-plus-one (x)
;; ":<docstring for double-plus-one>"
;; (+ :<a> :<b>))
;;
;; Then run `M-x hole-query-replace' to fill in the holes.
;;
;; The `llm' command must be installed and available in your PATH.
;;
;; See https://llm.datasette.io/ for installation instructions.
;;; Code:
(defconst hole-system-prompt
"Fill in the holes marked with tags like :<x>, :<blog post title>, etc.
Output a JSON object where field \"x\" is your best candidate for the :<x> hole, etc.
Insertions should be syntactically valid in their context.
For example, in Lisp they will often, but not always, be wrapped in parens.
Try to ignore holes that are themselves mere examples of the hole feature,
whenever that is obvious from the context.")
(defvar hole-pattern ":<\\([a-zA-Z -]+\\)>")
(defun hole-enable-highlighting ()
"Enable custom highlighting for `hole-mode'."
(font-lock-add-keywords
nil `((,hole-pattern (0 'widget-single-line-field prepend))) t))
(defun hole-disable-highlighting ()
"Disable custom highlighting for `hole-mode'."
(font-lock-remove-keywords
nil `((,hole-pattern (0 'widget-single-line-field prepend)))))
(define-minor-mode hole-mode
"Minor mode for filling holes with AI."
nil " :<>" nil
(if hole-mode
(progn
(hole-mode-enable-highlighting)
(font-lock-flush)
(font-lock-ensure))
(hole-mode-disable-highlighting)
(font-lock-flush)
(font-lock-ensure)))
(defun hole-replacement-function (data count)
"Replace holes in the buffer with values from the DATA alist."
(let* ((string (match-string 1))
(matched-key (intern string))
(replacement (cdr (assoc matched-key data))))
(replace-quote (or replacement string))))
(defun hole-do-query-replace (start end replacements)
"Execute a query-replace operation on code holes in a region."
(let ((query-flag t)
(regexp-flag t))
(save-excursion
(let ((start-marker (copy-marker start))
(end-marker (copy-marker end)))
(perform-replace hole-pattern
`(hole-replacement-function . ,replacements)
query-flag regexp-flag nil nil nil start-marker end-marker)))))
(defun hole-docstring-in-defun ()
"Insert a placeholder for a docstring in the current defun."
(interactive)
(save-excursion
(paredit-focus-on-defun)
(paredit-forward-down 1)
(paredit-forward 1)
(let ((name (save-excursion
(let ((start (point)))
(paredit-forward 1)
(buffer-substring-no-properties start (point))))))
(paredit-forward 2)
(insert "\n\":<docstring for" name ">\"")
(paredit-focus-on-defun)
(paredit-indent-sexps))))
(defun hole-check-presence-of-llm-command ()
;; We could easily do without the llm command by using the API directly...
"Check if the `llm' command is available."
(interactive)
(if (executable-find "llm")
t
(error "You need the `llm' command.
See https://llm.datasette.io/ for installation instructions.
Using `pipx' is recommended to avoid dependency conflicts")))
(defun hole-query-replace (start end)
"Query the user to fill in the holes in the selected region."
(interactive "r")
(hole-check-presence-of-llm-command)
(let ((code (buffer-substring start end))
(json-buffer (get-buffer-create "*Hole LLM Output*")))
(shell-command-on-region
start end
(concat "llm -m 4t -o json_object true -s "
(shell-quote-argument hole-system-prompt))
json-buffer nil shell-command-default-error-buffer t)
(save-excursion
(let ((replacements (with-current-buffer json-buffer
(goto-char (point-min))
(json-read))))
(hole-do-query-replace start end replacements)))))
(defun hole-git-commit-edit-hook ()
"Insert a placeholder for a commit message in a `git commit' buffer."
(when (string-match-p "COMMIT_EDITMSG" (buffer-name))
(hole-insert "commit title")
(move-end-of-line 1)
(newline 2)
(hole-insert "commit message")
(move-end-of-line 1)
(newline 1)
(goto-char (point-min))
(auto-fill-mode)
(delete-other-windows)))
(defun hole-magit-commit ()
"Start a `git commit' operation with holes to fill."
(interactive)
(add-hook 'find-file-hook 'hole-git-commit-edit-hook)
(magit-commit-create '("--verbose" "--all"))
(message "Select the entire buffer and run `M-x hole-query-replace'"))
(provide 'hole-mode)
;;; hole-mode.el ends here
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment