Skip to content

Instantly share code, notes, and snippets.

@mbrock
Last active May 30, 2024 07:03

Revisions

  1. mbrock revised this gist Feb 16, 2024. 1 changed file with 0 additions and 2 deletions.
    2 changes: 0 additions & 2 deletions hole-mode.el
    Original file line number Diff line number Diff line change
    @@ -144,6 +144,4 @@ Using `pipx' is recommended to avoid dependency conflicts")))

    (provide 'hole-mode)

    (provide 'hole-mode)

    ;;; hole-mode.el ends here
  2. mbrock revised this gist Feb 16, 2024. 1 changed file with 37 additions and 11 deletions.
    48 changes: 37 additions & 11 deletions hole-mode.el
    Original file line number Diff line number Diff line change
    @@ -1,10 +1,30 @@
    ;;; -*- lexical-binding: t -*-
    ;;; hole-mode.el --- fill holes with GPT-4
    ;;; hole-mode.el --- fill holes with GPT-4 -*- lexical-binding: t -*-

    ;; Author: Mikael Brockman <mikael@brockman.se>
    ;; Version: 1.0

    (defconst hole-system-prompt
    ;;; 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.
    @@ -19,16 +39,16 @@ whenever that is obvious from the context.")

    (defun hole-enable-highlighting ()
    "Enable custom highlighting for `hole-mode'."
    (font-lock-add-keywords
    (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
    (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"
    "Minor mode for filling holes with AI."
    nil " :<>" nil
    (if hole-mode
    (progn
    @@ -40,7 +60,7 @@ whenever that is obvious from the context.")
    (font-lock-ensure)))

    (defun hole-replacement-function (data count)
    "Replace holes in the buffer with values from a JSON object."
    "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))))
    @@ -83,17 +103,17 @@ whenever that is obvious from the context.")
    See https://llm.datasette.io/ for installation instructions.
    Using `pipx' is recommended to avoid dependency conflicts.")))
    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-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
    @@ -103,6 +123,7 @@ Using `pipx' is recommended to avoid dependency conflicts.")))
    (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)
    @@ -115,9 +136,14 @@ Using `pipx' is recommended to avoid dependency conflicts.")))
    (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)

    (provide 'hole-mode)

    ;;; hole-mode.el ends here
  3. mbrock revised this gist Feb 16, 2024. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion hole-mode.el
    Original file line number Diff line number Diff line change
    @@ -77,7 +77,7 @@ whenever that is obvious from the context.")
    ;; We could easily do without the llm command by using the API directly...
    "Check if the `llm' command is available."
    (interactive)
    (if (executable-find "llmx")
    (if (executable-find "llm")
    t
    (error "You need the `llm' command.
  4. mbrock revised this gist Feb 16, 2024. 1 changed file with 18 additions and 0 deletions.
    18 changes: 18 additions & 0 deletions hole-mode.el
    Original file line number Diff line number Diff line change
    @@ -102,4 +102,22 @@ Using `pipx' is recommended to avoid dependency conflicts.")))
    (json-read))))
    (hole-do-query-replace start end replacements)))))

    (defun hole-git-commit-edit-hook ()
    (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 ()
    (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)
  5. mbrock created this gist Feb 16, 2024.
    105 changes: 105 additions & 0 deletions hole-mode.el
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,105 @@
    ;;; -*- lexical-binding: t -*-
    ;;; hole-mode.el --- fill holes with GPT-4

    ;; Author: Mikael Brockman <mikael@brockman.se>
    ;; Version: 1.0

    (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 a JSON object."
    (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 "llmx")
    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)))))

    (provide 'hole-mode)