Last active
May 30, 2024 07:03
-
-
Save mbrock/3d9ffb70dd1938d767d7fcee95f61c69 to your computer and use it in GitHub Desktop.
hole mode for emacs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;;; hole-mode.el --- fill holes with GPT-4 -*- lexical-binding: t -*- | |
;; Author: Mikael Brockman <[email protected]> | |
;; 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