diff --git a/custom.el b/custom.el index 1f6ee7a..aed2a6d 100644 --- a/custom.el +++ b/custom.el @@ -5,17 +5,22 @@ ;; Your init file should contain only one such instance. ;; If there is more than one, they won't work right. '(package-selected-packages - '(aider browse-kill-ring caddyfile-mode cape cider company corfu - dockerfile-mode dumb-jump dumber-jump emacs-lldb emmet - emmet-mode exec-path-from-shell flycheck fsharp-mode - haskell-mode haskell-ts-mode helm htmlize idris-mode + '(aider browse-kill-ring caddyfile-mode cape cider cmake-mode company + corfu dart-mode dockerfile-mode dumb-jump dumber-jump + emmet-mode exec-path-from-shell expand-region flutter + flycheck fsharp-mode gptel haskell-mode haskell-ts-mode + helm htmlize idris-mode llm-tool-collection llm-tools marginalia markdown-ts-mode move-text multiple-cursors - odin-mode orderless org-mode parinfer-rust-mode - realgud-lldb rg sly terraform-mode tree-sitter-langs + ocaml-ts-mode orderless parinfer-rust-mode realgud-lldb + reason-mode rg sly terraform-mode tree-sitter-langs tuareg vertico visual-fill-column yasnippet-snippets zig-mode)) + '(package-vc-selected-packages + '((llm-tool-collection :url + "https://github.com/skissue/llm-tool-collection"))) '(safe-local-variable-directories '("/Users/grant/programming/project/edit/")) '(safe-local-variable-values - '((Package . CL-USER) (Syntax . ANSI-Common-Lisp) (Base . 10) + '((checkdoc-allow-quoting-nil-and-t . t) (Package . CL-USER) + (Syntax . ANSI-Common-Lisp) (Base . 10) (flycheck-clang-includes . "SDL3-3.2.26/include")))) (custom-set-faces ;; custom-set-faces was added by Custom. diff --git a/init.el b/init.el index 0723daf..06058b6 100644 --- a/init.el +++ b/init.el @@ -1,5 +1,7 @@ ;;; -*- lexical-binding: t; -*- +(setq gc-cons-threshold (* 800000 100)) + (require 'package) (add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t) @@ -10,10 +12,13 @@ ;; If startup times are slow ;; (setq use-package-verbose t) -;; (setq use-package-compute-statistics t) +(setq use-package-compute-statistics t) -(add-to-list 'load-path (concat user-emacs-directory "local-lisp/")) +(add-to-list 'load-path (concat user-emacs-directory "user-lisp/")) +(add-to-list 'load-path "/opt/homebrew/Cellar/mu/1.12.14/share/emacs/site-lisp/mu/mu4e") +(global-auto-revert-mode 1) +(recentf-mode 1) (setq tab-bar-show nil) (setq inhibit-startup-message t) (setq initial-scratch-message nil) @@ -39,6 +44,13 @@ (pixel-scroll-precision-mode 1) (kill-ring-deindent-mode 1) +(setopt auto-revert-avoid-polling t) +(setopt auto-revert-interval 5) +(setopt auto-revert-check-vc-info t) +(global-auto-revert-mode 1) +(setq global-auto-revert-non-file-buffers t) +(setq-default grep-command "rg") + (setq use-package-always-ensure t) (setq mac-command-modifier 'control) (setq is-mac (string= system-type "darwin")) @@ -51,13 +63,14 @@ (defun hgh/enable-cursor-blink () (blink-cursor-mode 1)) -(add-hook 'activate-mark-hook 'hgh/disable-cursor-blink) +(add-hook 'activate-mark-hook 'hgh/disable-cursor-blink) (add-hook 'deactivate-mark-hook 'hgh/enable-cursor-blink) (when is-mac (setq dired-use-ls-dired t insert-directory-program "gls" - dired-listing-switches "-aBhl --group-directories-first")) + dired-listing-switches "-aBhl --group-directories-first") + (setq-default manual-program "gman")) ;; Add .asdf to exec-path (when (file-exists-p (file-truename "~/.asdf")) @@ -74,6 +87,11 @@ (when exec-path (setenv "PATH" (string-join exec-path ":"))) +(defun hgh/duplicate-dwim () + (interactive) + (duplicate-dwim) + (next-line)) + (defun hgh/visit-init-file () (interactive) (find-file user-init-file)) @@ -110,7 +128,11 @@ (keymap-global-set "M-1" #'delete-other-windows) (keymap-global-set "M-2" #'split-window-below) (keymap-global-set "M-3" #'split-window-right) -(keymap-global-set "C-," #'duplicate-dwim) +(keymap-global-set "C-," #'hgh/duplicate-dwim) + +(keymap-global-set "C-c f n" #'flymake-goto-next-error) +(keymap-global-set "C-c f p" #'flymake-goto-prev-error) +(keymap-global-set "C-c f b" #'flymake-show-buffer-diagnostics) (keymap-set completion-list-mode-map "M-n" #'minibuffer-next-completion) (keymap-set completion-list-mode-map "M-p" #'minibuffer-previous-completion) @@ -123,34 +145,18 @@ (load-theme 'modus-vivendi t) -;; Let's prefer completion-preview for now if it's available -(if (version<= "30.1" emacs-version) - (use-package completion-preview - :ensure nil - :demand t - :bind - (:map completion-preview-active-mode-map - ("M-n" . completion-preview-next-candidate) - ("M-p" . completion-preview-preview-candidate)) - :config - (global-completion-preview-mode t)) - (use-package corfu - :custom - (corfu-auto t) - (corfu-cycle t) - :config - (global-corfu-mode 1))) +(use-package corfu + :custom + (corfu-auto t) + (corfu-cycle t) + :config + (global-corfu-mode 1)) (use-package exec-path-from-shell :when is-mac :config (exec-path-from-shell-initialize)) -(use-package man - :when is-mac - :custom - (manual-program "gman")) - (use-package magit :defer t) @@ -164,7 +170,7 @@ ("\\.jsx\\'" . tsx-ts-mode) ("\\.json\\'" . json-ts-mode)) :custom - (treesit-extra-load-path '("~/repos/tree-sitter-module/dist"))) + (treesit-extra-load-path `("~/repos/tree-sitter-module/dist" ,(concat user-emacs-directory "/tree-sitter")))) (use-package tree-sitter-langs :defer t) @@ -175,6 +181,7 @@ (use-package company) (defun enable-parinfer () + (require 's) (let ((buf (or (buffer-file-name) (buffer-name) ""))) (when (and (not (s-contains? "sbcl" buf)) @@ -191,13 +198,46 @@ (parinfer-rust-disable-troublesome-modes t)) (use-package sly - :mode "\\.lisp\\'" + :commands (sly) :custom (inferior-lisp-program "/opt/homebrew/bin/sbcl")) +(defun hgh/--project-execute (command name) + (require 'project) + (when (not command) + (user-error (concat "No command specified for project-" name))) + (let ((default-directory (project-root (project-current t)))) + (compilation-start command + nil + (lambda (mode-name) (concat "*" (project-name (project-current)) " " name "*"))))) + + + +(defmacro hgh/define-project-command (name) + "Define a project command with NAME. +Creates a variable `hgh/project-NAME-command' and function `hgh/project-NAME'." + (let ((var-sym (intern (format "hgh/project-%s-command" name))) + (fn-sym (intern (format "hgh/project-%s" name)))) + `(progn + (defvar ,var-sym nil + ,(format "Command to %s the project." name)) + (put ',var-sym 'safe-local-variable #'stringp) + (defun ,fn-sym () + ,(format "Execute the project %s command." name) + (interactive) + (hgh/--project-execute ,var-sym ,name))))) + + +(hgh/define-project-command "run") +(hgh/define-project-command "test") +(hgh/define-project-command "lint") + (use-package project :bind (("C-x p s" . hgh/project-ripgrep) - ("C-x p S" . project-shell))) + ("C-x p S" . project-shell) + ("C-x p t" . hgh/project-test) + ("C-x p r" . hgh/project-run) + ("C-x p l" . hgh/project-lint))) (use-package vertico :custom @@ -222,9 +262,6 @@ :init (add-to-list 'completion-at-point-functions #'cape-dabbrev)) -(use-package browse-kill-ring - :bind (("C-c y" . browse-kill-ring))) - (defun hgh/org-mode-visual-fill () (setq visual-fill-column-width 120 visual-fill-column-center-text t) @@ -245,7 +282,10 @@ (svelte-mode . eglot-ensure) (haskell-mode . eglot-ensure) (terraform-mode . eglot-ensure) - (odin-mode . eglot-ensure)) + (odin-mode . eglot-ensure) + (tuareg-mode . eglot-ensure) + (ruby-mode . eglot-ensure) + (dart-mode . eglot-ensure)) :bind (:map eglot-mode-map ("C-c r" . eglot-rename) @@ -257,21 +297,39 @@ :config ;; Set up using clippy with rust analyzer (setf eglot-server-programs - (cl-remove-if (lambda (c) (equal (car c) 'rust-mode)) + (cl-remove-if (lambda (c) (equal (car c) (or 'rust-mode 'tuareg-mode '(dart-mode dart-ts-mode)))) eglot-server-programs)) (setf eglot-server-programs (append (list (list '(rust-ts-mode rust-mode) "rust-analyzer" :initializationOptions '(:checkOnSave (:command "clippy"))) '(svelte-mode "svelteserver" "--stdio") - '(haskell-mode "haskell-language-server-wrapper" "--lsp")) - eglot-server-programs))) + '(haskell-mode "haskell-language-server-wrapper" "--lsp") + '(tuareg-mode "dune" "exec" "ocamllsp") + '(reason-mode "dune" "exec" "ocamllsp") + '(ruby-mode "solargraph" "socket" "--port" :autoport) + '(dart-mode "dart" "language-server" "--client-id" "emacs.eglot-dart") + eglot-server-programs)))) (setenv "DOTNET_ROOT" "~/.local/share/mise/installs/dotnet/9") -(push '("\\.csproj$" . xml-mode) auto-mode-alist) -(push '("\\.vs$" . c-mode) auto-mode-alist) -(push '("\\.fs$" . c-mode) auto-mode-alist) -(push '("\\.sbclrc" . lisp-mode) auto-mode-alist) +(push '("\\.csproj$" . xml-mode) auto-mode-alist) +(push '("\\.vs$" . c-mode) auto-mode-alist) +(push '("\\.fs$" . c-mode) auto-mode-alist) +(push '("\\.sbclrc" . lisp-mode) auto-mode-alist) +(push '("dune-project\\'" . lisp-mode) auto-mode-alist) +(push '("dune\\'" . lisp-mode) auto-mode-alist) +(push '("\\.ml\\'" . tuareg-mode) auto-mode-alist) +(setf auto-mode-alist + (cl-remove-if + (lambda (tup) + (or + (and + (string-equal (car tup) "\\.ml\\'") + (eq (cdr tup) 'ocaml-ts-mode)) + (and + (string-equal (car tup) "\\.ml\\'") + (eq (cdr tup) 'lisp-mode)))) + auto-mode-alist)) ;; paredit @@ -285,16 +343,10 @@ (define-key paredit-mode-map (kbd "M-:") #'paredit-backward-barf-sexp) (enable-paredit-mode)) -(defvar lisp-mode-hooks nil) -(setf lisp-mode-hooks '((emacs-lisp-mode-hook emacs-lisp-mode) - (clojure-mode-hook clojure-mode) - (lisp-mode-hook lisp-mode) - (sly-mode-hook sly-mode))) - (defun hgh/rg (search) (interactive "sSearch: ") (compilation-start - (concat "rg --no-heading '" search "'") + (concat "rg -S --no-heading '" search "'") 'compilation-mode (lambda (s) (concat @@ -317,7 +369,6 @@ (advice-add 'mc/mark-next-like-this :before #'hgh/set-cursor-type-box) (advice-add 'mc/mark-previous-like-this :before #'hgh/set-cursor-type-box)) - (use-package yasnippet :diminish :defer t @@ -365,26 +416,27 @@ (org-html-head-include-default-style nil) (org-html-head-include-scripts nil) (org-html-head "") + (org-capture-templates - '(("t" "Task" entry (file+headline "~/org/inbox.org" "Tasks") - "* TODO %?\n%U\n%i\n%a") - ("n" "Note" entry (file+headline "~/org/inbox.org" "Notes") - "* %?\n%U\n%i\n%a") - ("i" "Idea" entry (file+headline "~/org/inbox.org" "Ideas") - "* %?\n%U\n%i\n%a"))) + '(("t" "Task" entry (file+headline "~/org/inbox.org" "Tasks") + "* TODO %?\n%U\n%i\n%a") + ("n" "Note" entry (file+headline "~/org/inbox.org" "Notes") + "* %?\n%U\n%i\n%a") + ("i" "Idea" entry (file+headline "~/org/inbox.org" "Ideas") + "* %?\n%U\n%i\n%a"))) (org-publish-use-timestamps-flag nil) (org-publish-project-alist - (list - (list "writings" - :base-directory "~/Documents/writings/content" - :publishing-directory "~/Documents/writings/public" - :exclude "~/Documents/writings/notes" - :recursive t - :time-stamp-file nil - :section-numbers nil - :with-creator t - :with-author nil))) + (list + (list "writings" + :base-directory "~/Documents/writings/content" + :publishing-directory "~/Documents/writings/public" + :exclude "~/Documents/writings/notes" + :recursive t + :time-stamp-file nil + :section-numbers nil + :with-creator t + :with-author nil))) :config (require 'ox-publish) @@ -394,10 +446,12 @@ '((shell . t)))) (use-package dumber-jump + :defer t :init (add-hook 'xref-backend-functions #'dumber-jump-xref-activate)) -(use-package idris-mode) +(use-package idris-mode + :mode "\\.idr\\'") (use-package htmlize) @@ -444,3 +498,74 @@ js-mode ;; Need to add this tsx-ts-mode))) + +(use-package mu4e + :ensure nil + :commands (mu4e) + :custom + (mu4e-maildir "~/Mail") + (mu4e-get-mail-command "mbsync -a") + (mu4e-update-interval 300) + (mu4e-change-filenames-when-moving t) + (mu4e-context-policy 'pick-first) + (mu4e-compose-context-policy 'ask-if-none) + (mu4e-contexts + (list + (make-mu4e-context + :name "Grant" + :match-func + (lambda (msg) + (when msg + (string-prefix-p "/grant" (mu4e-message-field msg :maildir)))) + :vars '((user-mail-address . "grant@granthorner.dev") + (user-full-name . "Grant Horner") + (mu4e-drafts-folder . "/grant/Drafts") + (mu4e-sent-folder . "/grant/Sent Mail") + (mu4e-trash-folder . "/grant/Trash") + (mu4e-rstrash-folder . "/grant/Trash"))) + (make-mu4e-context + :name "Gmail" + :match-func + (lambda (msg) + (when msg + (string-prefix-p "/gmail" (mu4e-message-field msg :maildir)))) + :vars '((user-mail-address . "gmail@granthorner.dev") + (user-full-name . "Grant Horner") + (mu4e-drafts-folder . "/gmail/Drafts") + (mu4e-sent-folder . "/gmail/Sent Mail") + (mu4e-trash-folder . "/gmail/Trash") + (mu4e-rstrash-folder . "/gmail/Trash")))))) + +(use-package tuareg + :mode "\\.ml\\'") + +(defun hgh/eglot-restart () + (interactive) + (require 'eglot) + (when eglot--managed-mode + (message "Shutting down eglot servers...") + (eglot-shutdown-all) + (message "Restarting eglot for current buffer...") + (eglot))) + +(use-package reason-mode + :mode "\\.re\\'") + +(use-package expand-region + :bind ("C-=" . er/expand-region)) + +(use-package ediff + :defer t + :custom + (ediff-window-setup-function 'ediff-setup-windows-plain) + (ediff-split-window-function 'split-window-horizontally) + (ediff-diff-options "-w") + :config + (set-face-attribute 'ediff-odd-diff-A nil :background "#75383b") + (set-face-attribute 'ediff-even-diff-A nil :background "#75383b") + (set-face-attribute 'ediff-odd-diff-B nil :background "#265441") + (set-face-attribute 'ediff-even-diff-B nil :background "#265441")) + +(use-package dart-mode) + +(use-package flutter) diff --git a/local-lisp/codex.el b/user-lisp/codex.el similarity index 99% rename from local-lisp/codex.el rename to user-lisp/codex.el index af768b9..df7247a 100644 --- a/local-lisp/codex.el +++ b/user-lisp/codex.el @@ -83,7 +83,7 @@ http_headers = { :group 'codex) (defcustom codex-sessions-file (concat user-emacs-directory "codex-sessions") - "What the default name should be fore the codex chat buffer." + "What the default name should be for the codex chat buffer." :type 'string :group 'codex) diff --git a/user-lisp/llm-tools.el b/user-lisp/llm-tools.el new file mode 100644 index 0000000..53e6c56 --- /dev/null +++ b/user-lisp/llm-tools.el @@ -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 diff --git a/user-lisp/magit-ediff-all.el b/user-lisp/magit-ediff-all.el new file mode 100644 index 0000000..b27ebea --- /dev/null +++ b/user-lisp/magit-ediff-all.el @@ -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 diff --git a/local-lisp/odin-mode.el b/user-lisp/odin-mode.el similarity index 100% rename from local-lisp/odin-mode.el rename to user-lisp/odin-mode.el