move stuff around
This commit is contained in:
335
user-lisp/codex.el
Normal file
335
user-lisp/codex.el
Normal 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
346
user-lisp/llm-tools.el
Normal 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
|
||||
115
user-lisp/magit-ediff-all.el
Normal file
115
user-lisp/magit-ediff-all.el
Normal 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
310
user-lisp/odin-mode.el
Normal 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
|
||||
Reference in New Issue
Block a user