move stuff around

This commit is contained in:
2026-04-27 18:57:10 -04:00
parent e57257a3a5
commit 4c6caec1d7
6 changed files with 665 additions and 74 deletions

335
user-lisp/codex.el Normal file
View File

@@ -0,0 +1,335 @@
;;; codex.el --- Simple Codex chat buffer -*- lexical-binding: t; -*-
;; Copyright (C) 2025
;; Author: Grant Horner
;; Version: 0.1
;; Package-Requires: ((emacs "27.1"))
;; Keywords: tools, convenience
;;; Commentary:
;; Basic chat buffer for Codex using `codex exec --json`.
;; {"type":"thread.started","thread_id":"019ba390-af9c-7872-8617-c8038b61d559"}
;; {"type":"turn.started"}
;; {"type":"item.completed","item":{"id":"item_0","type":"reasoning","text":"**Responding with greeting**"}}
;; {"type":"item.completed","item":{"id":"item_1","type":"agent_message","text":"Hi! How can I help?"}}
;; {"type":"turn.completed","usage":{"input_tokens":4050,"cached_input_tokens":3712,"output_tokens":13}}
;;; Code:
(require 'json)
(require 'cl-lib)
(require 'subr-x)
(defgroup codex nil
"Chat with Codex."
:group 'tools)
(defface codex-prompt-delimiter-face
'((t :inherit shadow))
"Face for prompt delimiters."
:group 'codex)
(defface codex-user-prompt-face
'((t :inherit font-lock-keyword-face))
"Face for the user prompt label."
:group 'codex)
(defface codex-assistant-prompt-face
'((t :inherit font-lock-function-name-face))
"Face for the Codex prompt label."
:group 'codex)
(defcustom codex-command "codex"
"Codex executable to invoke."
:type 'string
:group 'codex)
(defcustom codex-provider nil
"Custom codex provider to use.
Note on Copilot:
In order for copilot integration to work, you need a section in your
~/.codex/config.toml like this:
[model_providers.github-copilot]
name = \"Github Copilot\"
base_url = \"https://api.githubcopilot.com\"
env_key = \"GITHUB_COPILOT_TOKEN\"
wire_api = \"chat\"
http_headers = {
Copilot-Integration-Id = \"vscode-chat\"
}"
:type 'string
:group 'codex)
(defcustom codex-model nil
"Model for Codex to use."
:type 'string
:group 'codex)
(defcustom codex-skip-git-repo-check t
"Whether to pass --skip-git-repo-check to Codex."
:type 'boolean
:group 'codex)
(defcustom codex-buffer-name "*codex*"
"What the default name should be fore the codex chat buffer."
:type 'string
:group 'codex)
(defcustom codex-wrap-lines t
"Whether to truncate long lines in `codex-mode`."
:type 'boolean
:group 'codex)
(defcustom codex-sessions-file (concat user-emacs-directory "codex-sessions")
"What the default name should be for the codex chat buffer."
:type 'string
:group 'codex)
(defcustom codex-sessions-history-dir
(concat user-emacs-directory "codex-sessions-history/")
"Directory where Codex session histories are stored."
:type 'directory
:group 'codex)
(defvar codex--session-id nil)
(defvar codex-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c C-c") #'codex-send)
(define-key map (kbd "C-c C-s") #'codex-switch-sessions)
map)
"Keymap for `codex-mode'.")
(defconst codex--font-lock-keywords
'(("^---$" . 'codex-prompt-delimiter-face)
("^User:$" . 'codex-user-prompt-face)
("^Codex:$" . 'codex-assistant-prompt-face)))
;;;###autoload
(define-derived-mode codex-mode fundamental-mode "Codex"
"Major mode for chatting with Codex."
(setq-local buffer-read-only nil)
(setq-local truncate-lines (not codex-wrap-lines))
(setq-local font-lock-defaults '(codex--font-lock-keywords))
(setq-local codex--busy nil))
(defvar-local codex--prompt-start nil)
(defun codex--ensure-history-dir ()
(make-directory codex-sessions-history-dir t))
(defun codex--history-file ()
(when (and codex--session-id (not (string-empty-p codex--session-id)))
(expand-file-name codex--session-id codex-sessions-history-dir)))
(defun codex--set-prompt-start-from-buffer ()
(let* ((haystack (buffer-string))
(needle "---\nUser:\n")
(index (cl-search needle haystack :from-end t)))
(when index
(setq codex--prompt-start (copy-marker (+ (length needle) index)))
t)))
(defun codex--save-session-history ()
(with-codex-buffer
(let ((history-file (codex--history-file)))
(when history-file
(codex--ensure-history-dir)
(write-region (point-min) (point-max) history-file nil 'silent)))))
(defun codex--load-session-history ()
(with-codex-buffer
(let ((history-file (codex--history-file)))
(setq codex--prompt-start nil)
(erase-buffer)
(if (and history-file (file-exists-p history-file))
(progn
(insert-file-contents history-file)
(goto-char (point-max))
(unless (codex--set-prompt-start-from-buffer)
(codex--user-prompt)))
(codex--user-prompt)))))
(defun list->hash-set (list &optional test)
"Return a hash table whose keys are the elements of LIST."
(let ((ht (make-hash-table :test (or test #'equal))))
(dolist (x list ht)
(puthash x t ht))))
(defmacro with-codex-buffer (&rest body)
`(with-current-buffer (get-buffer-create codex-buffer-name)
,@body))
(defun codex--prompt-delimiter ()
(newline)
(insert "---")
(newline))
(defun codex--user-prompt ()
(codex--prompt-delimiter)
(insert "User:")
(newline)
(setq codex--prompt-start (point-marker)))
(defun codex--write-to-chat (msg)
(with-codex-buffer
(newline)
(insert "Codex:")
(newline)
(insert msg)
(codex--user-prompt)
(codex--save-session-history)))
(defun codex--parse-session-id (jsons)
(let ((thread-started-message
(or (cl-find-if
(lambda (hm)
(and (string-equal (gethash "type" hm) "thread.started")
(gethash "thread_id" hm)))
jsons)
(make-hash-table))))
(gethash "thread_id" thread-started-message)))
(defun codex--parse-msg-from-response (response-string)
(let* ((output (string-trim response-string))
(lines (string-lines output))
(jsons (delq nil
(mapcar (lambda (line)
(condition-case nil
(json-parse-string line)
(json-parse-error nil)))
lines)))
(msg (cl-find-if (lambda (hm) (let ((type (gethash "type" hm))
(item (gethash "item" hm)))
(and type
item
(string-equal type "item.completed")
(string-equal (gethash "type" item) "agent_message"))))
jsons))
(item (and msg (gethash "item" msg)))
(text (and item (gethash "text" item))))
;; We should make sure we get one and only one message here, otherwise bail out
(setf codex--session-id (codex--parse-session-id jsons))
(or text output)))
(defun codex--ensure-session-in-sessions-file (prompt)
(let ((sessions (codex--read-sessions-file)))
(unless (member codex--session-id (mapcar #'cdr sessions))
(codex--write-sessions-file (cons (cons prompt codex--session-id) sessions)))))
(defun codex--send (prompt)
"Sends a prompt to codex."
(let* ((buf (generate-new-buffer " *codex--send*"))
(proc (make-process
:name "codex"
:buffer buf
:command
(let* ((resume (when (and codex--session-id (not (string-empty-p codex--session-id)))
(progn
(message "Using session %s" codex--session-id)
(list "resume" codex--session-id))))
(skip (and codex-skip-git-repo-check (list "--skip-git-repo-check")))
(provider (and codex-provider (list "-c" (concat "model_provider=" codex-provider))))
(model (and codex-model (list "-m" codex-model)))
(command (append (list codex-command "exec") resume skip provider model (list "--json" prompt))))
command))))
(set-process-sentinel
proc
(lambda (p event)
(message event)
(when (string= event "finished\n")
(with-current-buffer (process-buffer p)
(thread-first
(buffer-string)
codex--parse-msg-from-response
codex--write-to-chat)
(codex--ensure-session-in-sessions-file prompt)
(kill-buffer)))
(when (string-prefix-p "exited abnormally" event)
(with-current-buffer (process-buffer p)
(error (buffer-string))))))))
(defun codex--get-current-user-prompt ()
(with-codex-buffer
(let ((prompt-start (and codex--prompt-start
(marker-buffer codex--prompt-start)
(marker-position codex--prompt-start))))
;; fallback in case we somehow lost where our user prompt starts
(unless prompt-start
(when (codex--set-prompt-start-from-buffer)
(setq prompt-start (marker-position codex--prompt-start))))
(unless prompt-start
(error "No current prompt start"))
(buffer-substring-no-properties prompt-start (point-max)))))
(defun codex-send ()
(interactive)
(with-current-buffer codex-buffer-name
(let ((prompt (codex--get-current-user-prompt)))
(if (string-equal prompt "")
(error "Prompt is empty!")
(newline)
(insert "---")
(codex--save-session-history)
(codex--send prompt)))))
;;;###autoload
(defun codex (session)
"Open a Codex chat buffer."
(interactive
(list (completing-read "Session: " (cons '("New") (codex--read-sessions-file)))))
(let ((session-id (cdr (assoc session (codex--read-sessions-file)))))
(setf codex--session-id session-id)
(let ((buf (get-buffer-create codex-buffer-name)))
(pop-to-buffer buf)
(unless (derived-mode-p 'codex-mode)
(codex-mode))
(codex--load-session-history)
(goto-char (point-max)))))
(defun codex--read-sessions-file ()
(if (file-exists-p codex-sessions-file)
(with-temp-buffer
(insert-file-contents codex-sessions-file)
(goto-char (point-min))
(condition-case nil
(read (current-buffer))
(end-of-file nil)))
(codex--write-sessions-file '())
nil))
(defun codex--write-sessions-file (list-of-session-ids)
(let ((filename codex-sessions-file))
(with-temp-buffer
(insert ";;; -*- lisp-data -*-\n")
(let ((print-length nil)
(print-level nil))
(pp list-of-session-ids (current-buffer)))
(write-region nil nil filename nil 'silent))))
(defun codex-switch-sessions (new-session)
(interactive
(list (completing-read "Session: " (cons '("New") (codex--read-sessions-file)))))
(let ((session-id (cdr (assoc new-session (codex--read-sessions-file)))))
(codex--save-session-history)
(setf codex--session-id session-id)
(codex--load-session-history)))
(defun codex-rename-current-session (name)
(interactive "sNew name: ")
(let* ((sessions (codex--read-sessions-file))
(new-sessions
(mapcar
(lambda (tup)
(if (string-equal (cdr tup) codex--session-id)
(cons name (cdr tup))
tup))
sessions)))
(codex--write-sessions-file new-sessions)))
(provide 'codex)
;;; codex.el ends here

346
user-lisp/llm-tools.el Normal file
View File

@@ -0,0 +1,346 @@
;;; llm-tools.el --- Emacs tools for GPTel models -*- lexical-binding: t -*-
;; Copyright (C) 2024
;; Author: Your Name
;; Keywords: tools, convenience, gptel
;; This file is NOT part of GNU Emacs.
;;; Commentary:
;; This file provides GPTel tools that allow LLM models to look up
;; information in Emacs, including function names and documentation,
;; variable names and documentation, key bindings, and more.
;;; Code:
(require 'gptel)
;; ============================================================================
;; Documentation Lookup Tools
;; ============================================================================
(gptel-make-tool
:name "get_function_doc"
:category "emacs"
:description "Get the documentation string for an Emacs Lisp function. Returns the function's docstring, argument list, and source file location."
:function (lambda (function-name)
(let ((sym (intern-soft function-name)))
(if (and sym (fboundp sym))
(let* ((doc (documentation sym t))
(arglist (help-function-arglist sym))
(file (find-lisp-object-file-name sym 'defun)))
(format "Function: %s\nArguments: %s\nFile: %s\n\nDocumentation:\n%s"
sym
(if arglist (prin1-to-string arglist) "(unknown)")
(or file "built-in/unknown")
(or doc "No documentation available")))
(format "Error: Function '%s' not found" function-name))))
:args '((:name "function-name"
:type string
:description "The name of the function (symbol) to look up"
:optional nil)))
(gptel-make-tool
:name "get_variable_doc"
:category "emacs"
:description "Get the documentation string for an Emacs Lisp variable. Returns the variable's docstring, current value (if safe to display), and source file location."
:function (lambda (variable-name)
(let ((sym (intern-soft variable-name)))
(if (and sym (boundp sym))
(let* ((doc (documentation-property sym 'variable-documentation))
(file (find-lisp-object-file-name sym 'defvar))
(value (bound-and-true-p sym))
(value-str (if (and value (or (numberp value)
(stringp value)
(symbolp value)
(and (listp value)
(< (length (format "%s" value)) 200))))
(format "%S" value)
"(complex value omitted)")))
(format "Variable: %s\nCurrent Value: %s\nFile: %s\n\nDocumentation:\n%s"
sym
value-str
(or file "built-in/unknown")
(or doc "No documentation available")))
(format "Error: Variable '%s' not found" variable-name))))
:args '((:name "variable-name"
:type string
:description "The name of the variable (symbol) to look up"
:optional nil)))
(gptel-make-tool
:name "get_face_doc"
:category "emacs"
:description "Get information about an Emacs face including its documentation and attributes."
:function (lambda (face-name)
(let ((sym (intern-soft face-name)))
(if (and sym (facep sym))
(let* ((doc (documentation-property sym 'face-documentation))
(attrs (face-all-attributes sym (selected-frame))))
(format "Face: %s\n\nDocumentation:\n%s\n\nAttributes:\n%s"
sym
(or doc "No documentation available")
(mapconcat (lambda (attr)
(format " %s: %S" (car attr) (cdr attr)))
attrs
"\n")))
(format "Error: Face '%s' not found" face-name))))
:args '((:name "face-name"
:type string
:description "The name of the face to look up"
:optional nil)))
(gptel-make-tool
:name "describe_key"
:category "emacs"
:description "Get the command bound to a specific key sequence in Emacs."
:function (lambda (key-sequence)
(condition-case err
(let* ((keys (read-kbd-macro key-sequence))
(binding (key-binding keys))
(cmd (if (arrayp binding)
(format "Keymap: %S" binding)
binding)))
(if (and cmd (not (eq cmd 'nil)))
(format "Key: %s\nBound to: %s\n\nDocumentation:\n%s"
key-sequence
(if (symbolp cmd) cmd (format "%S" cmd))
(if (symbolp cmd)
(or (documentation cmd t) "No documentation")
"Not a command"))
(format "Key '%s' is not bound to any command" key-sequence)))
(error (format "Error parsing key sequence '%s': %s"
key-sequence
(error-message-string err)))))
:args '((:name "key-sequence"
:type string
:description "The key sequence as a string (e.g., 'C-x C-f')"
:optional nil)))
(gptel-make-tool
:name "apropos_lookup"
:category "emacs"
:description "Look up Emacs symbols using apropos. Searches for symbols matching a pattern and returns their names, types, and documentation snippets."
:function (lambda (pattern type)
(let* ((symbols (apropos-internal pattern (lambda (s) (fboundp s))))
(vars (apropos-internal pattern (lambda (s) (boundp s))))
(faces (apropos-internal pattern (lambda (s) (facep s))))
(filter-fn (cond
((string= type "functions") (lambda (s) (memq s symbols)))
((string= type "variables") (lambda (s) (memq s vars)))
((string= type "faces") (lambda (s) (memq s faces)))
(t (lambda (s) t))))
(all-symbols (delete-dups (append symbols vars faces)))
(filtered (seq-filter filter-fn all-symbols)))
(if filtered
(format "Found %d symbols matching '%s':\n\n%s"
(length filtered)
pattern
(mapconcat (lambda (sym)
(format "%s (%s)\n %s"
sym
(cond ((fboundp sym) "function")
((boundp sym) "variable")
((facep sym) "face")
(t "symbol"))
(let ((doc (or (documentation sym t)
(documentation-property sym 'variable-documentation)
(documentation-property sym 'face-documentation)
"No documentation")))
(if (> (length doc) 100)
(concat (substring doc 0 100) "...")
doc))))
filtered
"\n\n"))
(format "No symbols matching pattern '%s' found" pattern))))
:args '((:name "pattern"
:type string
:description "The regex pattern to search for in symbol names"
:optional nil)
(:name "type"
:type string
:description "Filter by type: 'functions', 'variables', 'faces', or 'all' (default 'all')"
:optional t
:default "all")))
;; ============================================================================
;; Buffer/List Tools
;; ============================================================================
(gptel-make-tool
:name "list_modes"
:category "emacs"
:description "List major and minor modes available in Emacs."
:function (lambda (mode-type pattern)
(let ((major-modes (apropos-internal "-mode\\'" (lambda (s)
(and (boundp s)
(get s 'derived-mode-parent)))))
(minor-modes (apropos-internal "-mode\\'" (lambda (s)
(and (boundp s)
(get s 'minor-mode-function)))))
(filter (when pattern
(lambda (s) (string-match-p pattern (symbol-name s))))))
(when filter
(setq major-modes (seq-filter filter major-modes))
(setq minor-modes (seq-filter filter minor-modes)))
(let ((results
(cond
((string= mode-type "major")
(cons "Major Modes" major-modes))
((string= mode-type "minor")
(cons "Minor Modes" minor-modes))
(t
`(("Major Modes" . ,major-modes)
("Minor Modes" . ,minor-modes))))))
(if (consp (car results))
(format "%s\n\n%s"
(format "%s (%d):\n%s"
(caar results)
(length (cdar results))
(mapconcat (lambda (m) (format " - %s" m))
(cdar results)
"\n"))
(format "%s (%d):\n%s"
(cadr results)
(length (cddr results))
(mapconcat (lambda (m) (format " - %s" m))
(cddr results)
"\n")))
(format "%s (%d):\n%s"
(car results)
(length (cdr results))
(mapconcat (lambda (m) (format " - %s" m))
(cdr results)
"\n"))))))
:args '((:name "mode-type"
:type string
:description "Type of mode: 'major', 'minor', or 'all' (default 'all')"
:optional t
:default "all")
(:name "pattern"
:type string
:description "Optional regex pattern to filter mode names"
:optional t)))
;; ============================================================================
;; Source Location Tools
;; ============================================================================
(gptel-make-tool
:name "find_function_source"
:category "emacs"
:description "Find the file where an Emacs Lisp function is defined. Returns the file path and line number if available."
:function (lambda (function-name)
(let ((sym (intern-soft function-name)))
(if (and sym (fboundp sym))
(let* ((file (find-lisp-object-file-name sym 'defun))
(source (when file
(with-temp-buffer
(insert-file-contents file)
(goto-char (point-min))
(re-search-forward (format "^(defun\\s +%s" (regexp-quote function-name)) nil t)
(when (re-search-backward "^(defun\\s +" nil t)
(cons file (line-number-at-pos)))))))
(if source
(format "Function '%s' is defined in:\n %s:%d"
function-name (car source) (cdr source))
(format "Function '%s' source location: %s"
function-name (or file "built-in/unknown"))))
(format "Error: Function '%s' not found" function-name))))
:args '((:name "function-name"
:type string
:description "The name of the function to locate"
:optional nil)))
(gptel-make-tool
:name "find_variable_source"
:category "emacs"
:description "Find the file where an Emacs Lisp variable is defined. Returns the file path if available."
:function (lambda (variable-name)
(let ((sym (intern-soft variable-name)))
(if (and sym (boundp sym))
(let ((file (or (find-lisp-object-file-name sym 'defvar)
(find-lisp-object-file-name sym 'defcustom)
(find-lisp-object-file-name sym 'defconst))))
(format "Variable '%s' is defined in: %s"
variable-name (or file "built-in/unknown")))
(format "Error: Variable '%s' not found" variable-name))))
:args '((:name "variable-name"
:type string
:description "The name of the variable to locate"
:optional nil)))
;; ============================================================================
;; Current Context Tools
;; ============================================================================
(gptel-make-tool
:name "get_buffer_context"
:category "emacs"
:description "Get information about the current buffer including its name, major mode, minor modes, and file path if applicable."
:function (lambda ()
(format "Buffer Context:\n Name: %s\n File: %s\n Major Mode: %s\n Minor Modes: %s\n Read-only: %s\n Modified: %s"
(buffer-name (current-buffer))
(or (buffer-file-name (current-buffer)) "N/A")
major-mode
(mapconcat (lambda (m) (format "%s" m))
(seq-filter (lambda (m) (and (boundp m) (symbol-value m)))
minor-mode-list)
", ")
(if buffer-read-only "yes" "no")
(if (buffer-modified-p) "yes" "no")))
:args nil)
(gptel-make-tool
:name "get_active_features"
:category "emacs"
:description "Get information about active Emacs features and packages."
:function (lambda ()
(format "Active Features (loaded packages/modules):\n%s"
(mapconcat (lambda (f) (format " - %s" f))
features
"\n")))
:args nil)
;; ============================================================================
;; Tool Registry for Easy Enable/Disable
;; ============================================================================
(defconst llm-tools-tool-list
'("get_function_doc"
"get_variable_doc"
"get_face_doc"
"describe_key"
"apropos_lookup"
"list_modes"
"find_function_source"
"find_variable_source"
"get_buffer_context"
"get_active_features")
"List of all LLM tool symbols defined in this file.")
;;;###autoload
(defun llm-tools-enable-all ()
"Enable all LLM Emacs tools for GPTel."
(interactive)
(dolist (tool llm-tools-tool-list)
(when (and tool (gptel-get-tool tool))
(add-to-list 'gptel-tools (gptel-get-tool tool))))
(message "LLM Emacs tools enabled for GPTel (%d tools)"
(length gptel-tools)))
;;;###autoload
(defun llm-tools-disable-all ()
"Disable all LLM Emacs tools from GPTel."
(interactive)
(dolist (tool llm-tools-tool-list)
(let ((tool-obj (gptel-get-tool tool)))
(when tool-obj
(setq gptel-tools (delete tool-obj gptel-tools)))))
(message "LLM Emacs tools disabled from GPTel"))
(provide 'llm-tools)
;;; llm-tools.el ends here

View File

@@ -0,0 +1,115 @@
;;; magit-ediff-all.el --- Compare all changed files between revisions using Ediff -*- lexical-binding:t -*-
;; Copyright (C) 2025 The Magit Project Contributors
;; Author: [Your Name]
;; Maintainer: [Your Name]
;; SPDX-License-Identifier: GPL-3.0-or-later
;;; Commentary:
;; This library extends Magit's ediff support by adding a command to
;; compare all changed files between two revisions using an ediff
;; session group.
;;; Code:
(require 'magit)
(require 'magit-ediff)
(require 'ediff)
(require 'cl-lib)
;; Define the new suffix key and description
(defvar magit-ediff-all-key "A"
"Key binding for the ediff all command in the magit-ediff transient.")
(defvar magit-ediff-all-description "All changed files (between revisions)"
"Description for the ediff all command in the magit-ediff transient.")
(defun magit-ediff-all--write-file (temp-files temp-buffers rev dir file)
(let* ((rel-path (if (file-name-absolute-p file)
(file-relative-name file (magit-toplevel))
file))
(rel-file (expand-file-name rel-path dir))
;; Handle files that are added or removed - use empty buffer
(buf (or (magit-find-file-noselect rev file)
(generate-new-buffer " *empty*"))))
;; Create necessary subdirectories
(let ((dir-part (file-name-directory rel-file)))
(when dir-part
(make-directory dir-part t)))
(push rel-file temp-files)
(push buf temp-buffers)
(with-current-buffer buf
(write-region (point-min) (point-max) rel-file nil 'quiet)
(add-hook 'ediff-prepare-buffer-hook
(lambda ()
(rename-buffer (format "%s~%s~" rel-file rev)))
0 t))))
;;;###autoload
(defun magit-ediff-all-files (revA revB)
"Compare all changed files between REVA and REVB using Ediff.
This creates an Ediff session group that allows you to navigate
through all changed files between the two revisions and compare
them one at a time."
(interactive
(pcase-let ((`(,a ,b) (magit-ediff-compare--read-revisions nil)))
(list a b)))
(when (or (not revA) (not revB))
(user-error "Both revisions must be specified"))
(magit-with-toplevel
(let* ((changed-files (magit-changed-files revA revB))
(dirA (make-temp-file (format "magit-ediff-%s-" revA) t))
(dirB (make-temp-file (format "magit-ediff-%s-" revB) t))
temp-files
temp-buffers)
(message "Preparing %d files for comparison..." (length changed-files))
;; Create temporary files for each revision
(dolist (file changed-files)
(magit-ediff-all--write-file temp-files temp-buffers revA dirA file)
(magit-ediff-all--write-file temp-files temp-buffers revB dirB file))
;; Define cleanup function
(cl-labels
((magit-ediff-all--cleanup ()
(dolist (file temp-files)
(when (file-exists-p file)
(delete-file file)))
;; Kill any buffers visiting files in the temp directories
(dolist (buf (buffer-list))
(when-let ((filename (buffer-file-name buf)))
(when (or (string-prefix-p (expand-file-name dirA) filename)
(string-prefix-p (expand-file-name dirB) filename))
(when (buffer-live-p buf)
(kill-buffer buf)))))
(when (file-exists-p dirA)
(delete-directory dirA t))
(when (file-exists-p dirB)
(delete-directory dirB t))
(dolist (buf temp-buffers)
(when (buffer-live-p buf)
(kill-buffer buf)))
(remove-hook 'ediff-quit-session-group-hook #'magit-ediff-all--cleanup)))
;; Register hooks
(add-hook 'ediff-quit-session-group-hook #'magit-ediff-all--cleanup)
;; Open ediff on the two directories
(message "Opening Ediff session group...")
(ediff-directories dirA dirB nil)))))
;; Add the command to the magit-ediff transient
(with-eval-after-load 'magit-ediff
(transient-append-suffix 'magit-ediff "r"
`(,magit-ediff-all-key ,magit-ediff-all-description magit-ediff-all-files)))
(provide 'magit-ediff-all)
;;; magit-ediff-all.el ends here

310
user-lisp/odin-mode.el Normal file
View File

@@ -0,0 +1,310 @@
;;; odin-mode.el --- A minor mode for odin
;; Author: Ethan Morgan
;; Keywords: odin, language, languages, mode
;; Package-Requires: ((emacs "24.1"))
;; Homepage: https://github.com/glassofethanol/odin-mode
;; This file is NOT part of GNU Emacs.
;;; Code:
(require 'cl-lib)
(require 'rx)
(require 'js)
(defgroup odin nil
"Odin mode"
:group 'languages)
;; `compilation-mode' configuration
(eval-after-load 'compile
'(add-to-list 'compilation-error-regexp-alist '("^\\(.*?\\)(\\([0-9]+\\):\\([0-9]+\\).*" 1 2 3)))
(defconst odin-mode-syntax-table
(let ((table (make-syntax-table)))
(modify-syntax-entry ?\" "\"" table)
(modify-syntax-entry ?\\ "\\" table)
;; additional symbols
(modify-syntax-entry ?' "\"" table)
(modify-syntax-entry ?` "\"" table)
(modify-syntax-entry ?: "." table)
(modify-syntax-entry ?+ "." table)
(modify-syntax-entry ?- "." table)
(modify-syntax-entry ?% "." table)
(modify-syntax-entry ?& "." table)
(modify-syntax-entry ?| "." table)
(modify-syntax-entry ?^ "." table)
(modify-syntax-entry ?! "." table)
(modify-syntax-entry ?$ "." table)
(modify-syntax-entry ?= "." table)
(modify-syntax-entry ?< "." table)
(modify-syntax-entry ?> "." table)
(modify-syntax-entry ?? "." table)
;; Need this for #directive regexes to work correctly
(modify-syntax-entry ?# "_" table)
;; Modify some syntax entries to allow nested block comments
(modify-syntax-entry ?/ ". 124b" table)
(modify-syntax-entry ?* ". 23n" table)
(modify-syntax-entry ?\n "> b" table)
(modify-syntax-entry ?\^m "> b" table)
table))
(defconst odin-builtins
'("len" "cap"
"typeid_of" "type_info_of"
"swizzle" "complex" "real" "imag" "quaternion" "conj"
"jmag" "kmag"
"min" "max" "abs" "clamp"
"expand_to_tuple"
"init_global_temporary_allocator"
"copy" "pop" "unordered_remove" "ordered_remove" "clear" "reserve"
"resize" "new" "new_clone" "free" "free_all" "delete" "make"
"clear_map" "reserve_map" "delete_key" "append_elem" "append_elems"
"append" "append_string" "clear_dynamic_array" "reserve_dynamic_array"
"resize_dynamic_array" "incl_elem" "incl_elems" "incl_bit_set"
"excl_elem" "excl_elems" "excl_bit_set" "incl" "excl" "card"
"assert" "panic" "unimplemented" "unreachable"))
(defconst odin-keywords
'("import" "foreign" "package"
"where" "when" "if" "else" "for" "switch" "in" "notin" "do" "case"
"break" "continue" "fallthrough" "defer" "return" "proc"
"struct" "union" "enum" "bit_field" "bit_set" "map" "dynamic"
"auto_cast" "cast" "transmute" "distinct" "opaque"
"using" "inline" "no_inline"
"size_of" "align_of" "offset_of" "type_of"
"context"
;; "_"
;; Reserved
"macro" "const"))
(defconst odin-constants
'("nil" "true" "false"
"ODIN_OS" "ODIN_ARCH" "ODIN_ENDIAN" "ODIN_VENDOR"
"ODIN_VERSION" "ODIN_ROOT" "ODIN_DEBUG"))
(defconst odin-typenames
'("bool" "b8" "b16" "b32" "b64"
"int" "i8" "i16" "i32" "i64"
"i16le" "i32le" "i64le"
"i16be" "i32be" "i64be"
"i128" "u128"
"i128le" "u128le"
"i128be" "u128be"
"uint" "u8" "u16" "u32" "u64"
"u16le" "u32le" "u64le"
"u16be" "u32be" "u64be"
"f32" "f64"
"complex64" "complex128"
"quaternion128" "quaternion256"
"rune"
"string" "cstring"
"uintptr" "rawptr"
"typeid" "any"
"byte"))
(defconst odin-attributes
'("builtin"
"export"
"static"
"deferred_in" "deferred_none" "deferred_out"
"require_results"
"default_calling_convention" "link_name" "link_prefix"
"deprecated" "private" "thread_local"))
(defconst odin-proc-directives
'("#force_inline"
"#force_no_inline"
"#type")
"Directives that can appear before a proc declaration")
(defconst odin-directives
(append '("#align" "#packed"
"#any_int"
"#raw_union"
"#no_nil"
"#complete"
"#no_alias"
"#c_vararg"
"#assert"
"#file" "#line" "#location" "#procedure" "#caller_location"
"#load"
"#defined"
"#bounds_check" "#no_bounds_check"
"#partial") odin-proc-directives))
(defun odin-wrap-word-rx (s)
(concat "\\<" s "\\>"))
(defun odin-wrap-keyword-rx (s)
(concat "\\(?:\\S.\\_<\\|\\`\\)" s "\\_>"))
(defun odin-wrap-directive-rx (s)
(concat "\\_<" s "\\>"))
(defun odin-wrap-attribute-rx (s)
(concat "[[:space:]\n]*@[[:space:]\n]*(?[[:space:]\n]*" s "\\>"))
(defun odin-keywords-rx (keywords)
"build keyword regexp"
(odin-wrap-keyword-rx (regexp-opt keywords t)))
(defun odin-directives-rx (directives)
(odin-wrap-directive-rx (regexp-opt directives t)))
(defun odin-attributes-rx (attributes)
(odin-wrap-attribute-rx (regexp-opt attributes t)))
(defconst odin-identifier-rx "[[:word:][:multibyte:]_]+")
(defconst odin-hat-type-rx (rx (group (and "^" (1+ (any word "." "_"))))))
(defconst odin-dollar-type-rx (rx (group "$" (or (1+ (any word "_")) (opt "$")))))
(defconst odin-number-rx
(rx (and
symbol-start
(or (and (+ digit) (opt (and (any "eE") (opt (any "-+")) (+ digit))))
(and "0" (any "xX") (+ hex-digit)))
(opt (and (any "_" "A-Z" "a-z") (* (any "_" "A-Z" "a-z" "0-9"))))
symbol-end)))
(defconst odin-proc-rx (concat "\\(\\_<" odin-identifier-rx "\\_>\\)\\s *::\\s *\\(" (odin-directives-rx odin-proc-directives) "\\)?\\s *\\_<proc\\_>"))
(defconst odin-type-rx (concat "\\_<\\(" odin-identifier-rx "\\)\\s *::\\s *\\(?:struct\\|enum\\|union\\|distinct\\)\\s *\\_>"))
(defconst odin-font-lock-defaults
`(
;; Types
(,odin-hat-type-rx 1 font-lock-type-face)
(,odin-dollar-type-rx 1 font-lock-type-face)
(,(odin-keywords-rx odin-typenames) 1 font-lock-type-face)
(,odin-type-rx 1 font-lock-type-face)
;; Hash directives
(,(odin-directives-rx odin-directives) 1 font-lock-preprocessor-face)
;; At directives
(,(odin-attributes-rx odin-attributes) 1 font-lock-preprocessor-face)
;; Keywords
(,(odin-keywords-rx odin-keywords) 1 font-lock-keyword-face)
;; single quote characters
("'\\(\\\\.\\|[^']\\)'" . font-lock-constant-face)
;; Variables
(,(odin-keywords-rx odin-builtins) 1 font-lock-builtin-face)
;; Constants
(,(odin-keywords-rx odin-constants) 1 font-lock-constant-face)
;; Strings
;; ("\\\".*\\\"" . font-lock-string-face)
;; Numbers
(,(odin-wrap-word-rx odin-number-rx) . font-lock-constant-face)
;; Procedures
(,odin-proc-rx 1 font-lock-function-name-face)
("---" . font-lock-constant-face)
("\\.\\.<" . font-lock-constant-face)
("\\.\\." . font-lock-constant-face)
))
;; add setq-local for older emacs versions
(unless (fboundp 'setq-local)
(defmacro setq-local (var val)
`(set (make-local-variable ',var) ,val)))
(defconst odin--defun-rx "\(.*\).*\{")
(defmacro odin-paren-level ()
`(car (syntax-ppss)))
(defun odin-line-is-defun ()
"return t if current line begins a procedure"
(interactive)
(save-excursion
(beginning-of-line)
(let (found)
(while (and (not (eolp)) (not found))
(if (looking-at odin--defun-rx)
(setq found t)
(forward-char 1)))
found)))
(defun odin-beginning-of-defun (&optional count)
"Go to line on which current function starts."
(interactive)
(let ((orig-level (odin-paren-level)))
(while (and
(not (odin-line-is-defun))
(not (bobp))
(> orig-level 0))
(setq orig-level (odin-paren-level))
(while (>= (odin-paren-level) orig-level)
(skip-chars-backward "^{")
(backward-char))))
(if (odin-line-is-defun)
(beginning-of-line)))
(defun odin-end-of-defun ()
"Go to line on which current function ends."
(interactive)
(let ((orig-level (odin-paren-level)))
(when (> orig-level 0)
(odin-beginning-of-defun)
(end-of-line)
(setq orig-level (odin-paren-level))
(skip-chars-forward "^}")
(while (>= (odin-paren-level) orig-level)
(skip-chars-forward "^}")
(forward-char)))))
(defalias 'odin-parent-mode
(if (fboundp 'prog-mode) 'prog-mode 'fundamental-mode))
;;;###autoload
(define-derived-mode odin-mode odin-parent-mode "Odin"
:syntax-table odin-mode-syntax-table
:group 'odin
(setq bidi-paragraph-direction 'left-to-right)
(setq-local require-final-newline mode-require-final-newline)
(setq-local parse-sexp-ignore-comments t)
(setq-local comment-start-skip "\\(//+\\|/\\*+\\)\\s *")
(setq-local comment-start "//")
(setq-local comment-end "")
(setq-local indent-line-function 'js-indent-line)
(setq-local font-lock-defaults '(odin-font-lock-defaults))
(setq-local beginning-of-defun-function 'odin-beginning-of-defun)
(setq-local end-of-defun-function 'odin-end-of-defun)
(setq-local electric-indent-chars
(append "{}():;," electric-indent-chars))
(setq imenu-generic-expression
`(("type" ,(concat "^" odin-type-rx) 1)
("proc" ,(concat "^" odin-proc-rx) 1)))
(font-lock-ensure))
;;;###autoload
(add-to-list 'auto-mode-alist '("\\.odin\\'" . odin-mode))
(provide 'odin-mode)
;;; odin-mode.el ends here