summaryrefslogtreecommitdiff
path: root/.config/emacs
diff options
context:
space:
mode:
authorThomas Voss <mail@thomasvoss.com> 2026-03-22 17:05:21 +0100
committerThomas Voss <mail@thomasvoss.com> 2026-03-22 17:05:21 +0100
commitf0e047b0674817c3fba38386a151c0b2ed620f50 (patch)
tree38598c348f7f536c2f0c302693856349639295d6 /.config/emacs
parent9a40ca3f9f198cb14c64c25edcf7c325ca2b8e5e (diff)
parentcb6831070008c56d3f287c4c754526d2c9d735a2 (diff)
Merge branch 'master' of github.com:Mango0x45/dotfilesHEADmaster
Diffstat (limited to '.config/emacs')
-rw-r--r--.config/emacs/init.el9
-rw-r--r--.config/emacs/mango-theme.el6
-rw-r--r--.config/emacs/modules/mm-completion.el99
-rw-r--r--.config/emacs/modules/mm-editing.el17
-rw-r--r--.config/emacs/modules/mm-humanwave.el93
-rw-r--r--.config/emacs/modules/mm-keybindings.el7
-rw-r--r--.config/emacs/modules/mm-search.el6
-rw-r--r--.config/emacs/site-lisp/grab.el211
8 files changed, 440 insertions, 8 deletions
diff --git a/.config/emacs/init.el b/.config/emacs/init.el
index eb0f23e..0f917c7 100644
--- a/.config/emacs/init.el
+++ b/.config/emacs/init.el
@@ -73,6 +73,15 @@ This function is meant to be used in conjuction with `read-string' and
(string-to-number string-or-number)
string-or-number))
+(defun mm-camel-to-lisp (string)
+ "Convert STRING from camelCase to lisp-case."
+ (declare (ftype (function (string) string))
+ (pure t) (side-effect-free t))
+ (let ((case-fold-search nil))
+ (downcase
+ (replace-regexp-in-string
+ (rx (group (or lower digit)) (group upper)) "\\1-\\2" string))))
+
(defun mm-do-and-center (function &rest arguments)
"Call FUNCTION with ARGUMENTS and then center the screen."
(apply function arguments)
diff --git a/.config/emacs/mango-theme.el b/.config/emacs/mango-theme.el
index 5fec2e0..f18f4c0 100644
--- a/.config/emacs/mango-theme.el
+++ b/.config/emacs/mango-theme.el
@@ -175,6 +175,12 @@ graphically, so I shouldn’t need to have multiple specs per face.
(marginalia-documentation
:foreground ,(mango-theme--color 'disabled)
:underline nil)
+ (mm-diffstat-counter-added
+ :foreground "green"
+ :weight bold)
+ (mm-diffstat-counter-removed
+ :foreground "red"
+ :weight bold)
;; Tempel
(tempel-default
diff --git a/.config/emacs/modules/mm-completion.el b/.config/emacs/modules/mm-completion.el
index 79da064..3367aa1 100644
--- a/.config/emacs/modules/mm-completion.el
+++ b/.config/emacs/modules/mm-completion.el
@@ -20,10 +20,80 @@
;;; Annotate Completions
-;; TODO: Show git branch descriptions!
(use-package marginalia
:ensure t
:hook after-init
+ :config
+ (with-eval-after-load 'magit
+ (defvar mm-marginalia--magit-cache nil)
+ (add-hook 'minibuffer-setup-hook
+ (lambda () (setq mm-marginalia--magit-cache nil)))
+
+ (defvar-local mm-marginalia-magit-base-branch "master")
+
+ (defface mm-diffstat-counter-added
+ '((t :inherit magit-diffstat-added))
+ "TODO")
+ (defface mm-diffstat-counter-removed
+ '((t :inherit magit-diffstat-removed))
+ "TODO")
+
+ (defun mm-marginalia-populate-magit-cache ()
+ "Batch-fetch all Git branch descriptions and stats into the cache."
+ (setq mm-marginalia--magit-cache (make-hash-table :test #'equal))
+ (when-let ((default-directory (magit-toplevel)))
+ (dolist (line (magit-git-lines "config" "list"))
+ (when (string-match "^branch\\.\\(.*?\\)\\.description=\\(.*\\)$" line)
+ (puthash (match-string 1 line)
+ (list :desc (match-string 2 line) :stats "")
+ mm-marginalia--magit-cache)))
+ (dolist (line (magit-git-lines
+ "for-each-ref"
+ (format
+ "--format=%%(refname:short)\x1F%%(ahead-behind:%s)"
+ mm-marginalia-magit-base-branch)
+ "refs/heads/"))
+ (when (string-match (rx bol (group (1+ (not #x1F)))
+ #x1F (group (1+ digit))
+ " " (group (1+ digit)) eol)
+ line)
+ (let* ((branch (match-string 1 line))
+ (ahead (+ (string-to-number (match-string 2 line))))
+ (behind (- (string-to-number (match-string 3 line))))
+ (ahead-str (if (zerop ahead)
+ ""
+ (propertize (format "%+d" ahead)
+ 'face 'mm-diffstat-counter-added)))
+ (behind-str (if (zerop behind)
+ ""
+ (propertize (format "%+d" behind)
+ 'face 'mm-diffstat-counter-removed)))
+ (stats-str (format "%5s %5s" ahead-str behind-str))
+ (existing (gethash branch mm-marginalia--magit-cache
+ (list :desc "" :stats ""))))
+ (puthash branch (plist-put existing :stats stats-str)
+ mm-marginalia--magit-cache))))))
+
+ (defun mm-marginalia-annotate-magit-branch (cand)
+ "Annotate Git branch CAND with ahead/behind stats and description."
+ (unless mm-marginalia--magit-cache
+ (mm-marginalia-populate-magit-cache))
+ (let* ((data (gethash cand mm-marginalia--magit-cache '(:desc "" :stats "")))
+ (desc (or (plist-get data :desc) ""))
+ (stats (or (plist-get data :stats) "")))
+ (marginalia--fields
+ (stats :width 10)
+ (desc :truncate 1.0 :face 'marginalia-documentation))))
+
+ (add-to-list 'marginalia-annotators
+ '(magit-branch mm-marginalia-annotate-magit-branch builtin none))
+ (dolist (cmd '(magit-branch-and-checkout
+ magit-branch-checkout
+ magit-branch-delete
+ magit-checkout
+ magit-merge
+ magit-rebase-branch))
+ (add-to-list 'marginalia-command-categories (cons cmd 'magit-branch))))
:custom
(marginalia-field-width 50)
(marginalia-max-relative-age 0))
@@ -158,4 +228,29 @@
:custom
(find-library-include-other-files nil))
-(provide 'mm-completion) \ No newline at end of file
+
+;;; Completion at Point Functions
+
+(defun mm-cape-file--not-dot-path-p (cand)
+ (declare (ftype (function (string) boolean))
+ (pure t) (side-effect-free t))
+ (not (or (string= cand "./")
+ (string= cand "../"))))
+
+(use-package cape
+ :ensure t
+ :init
+ (add-hook 'completion-at-point-functions
+ (cape-capf-predicate #'cape-file #'mm-cape-file--not-dot-path-p))
+ (add-hook 'completion-at-point-functions
+ (cape-capf-prefix-length #'cape-dabbrev 3)))
+
+
+;;; Completion at Point Live Completions
+
+(use-package completion-preview
+ :hook (after-init . global-completion-preview-mode)
+ :custom
+ (completion-preview-minimum-symbol-length 1))
+
+(provide 'mm-completion)
diff --git a/.config/emacs/modules/mm-editing.el b/.config/emacs/modules/mm-editing.el
index 2ae4107..e15e739 100644
--- a/.config/emacs/modules/mm-editing.el
+++ b/.config/emacs/modules/mm-editing.el
@@ -136,22 +136,29 @@ those should be listed in `mm-editing-indentation-settings'."
;;; Code Commenting
-(defun mm-c-comment-no-continue ()
+(defun mm-newcomment-c-config ()
(setq-local comment-continue " "))
-(defun mm-mhtml-comment-no-continue ()
+(defun mm-newcomment-html-config ()
(setq-local comment-continue " "))
+(defun mm-newcomment-rust-config ()
+ (setq-local comment-start "/* "
+ comment-end " */"
+ comment-continue " * " ; rustfmt doesn’t play nice
+ comment-quote-nested nil))
+
(use-package newcomment
:custom
(comment-style 'multi-line)
:config
(dolist (mode '(c-mode c++-mode))
- (add-hook (mm-mode-to-hook mode) #'mm-c-comment-no-continue)
+ (add-hook (mm-mode-to-hook mode) #'mm-newcomment-c-config)
(when-let ((ts-mode (mm-mode-to-ts-mode mode))
((fboundp ts-mode)))
- (add-hook (mm-mode-to-hook ts-mode) #'mm-c-comment-no-continue)))
- (add-hook 'mhtml-mode #'mm-mhtml-comment-no-continue))
+ (add-hook (mm-mode-to-hook ts-mode) #'mm-newcomment-c-config)))
+ (add-hook 'mhtml-mode #'mm-newcomment-html-config)
+ (add-hook 'rust-ts-mode #'mm-newcomment-rust-config))
;;; Multiple Cursors
diff --git a/.config/emacs/modules/mm-humanwave.el b/.config/emacs/modules/mm-humanwave.el
index f9e59b4..54e4a4b 100644
--- a/.config/emacs/modules/mm-humanwave.el
+++ b/.config/emacs/modules/mm-humanwave.el
@@ -158,4 +158,97 @@ to the `project-find-file' command."
(message "%s" path)
path))))
+(defun mm-humanwave-insert-last-commit-message ()
+ "TODO"
+ (interactive)
+ (insert
+ (with-temp-buffer
+ (call-process "git" nil t nil "log" "-1" "--pretty=%s")
+ (goto-char (point-min))
+ (replace-regexp "\\`HW-[0-9]+ " "")
+ (string-trim (buffer-string)))))
+
+
+;;; Jira Integration
+
+(use-package jira
+ :ensure t
+ :custom
+ (jira-api-version 3)
+ (jira-base-url "https://humanwave.atlassian.net")
+ (jira-detail-show-announcements nil)
+ (jira-issues-max-results 100)
+ (jira-issues-table-fields '(:key :status-name :assignee-name :summary))
+ (jira-token-is-personal-access-token nil))
+
+
+;;; Icon Autocompletion
+
+(defvar mm-humanwave-icon-component-file "web/src/components/icon.vue"
+ "Path to the <icon /> component definition.")
+
+(defun mm-humanwave--find-icon-map ()
+ (let* ((project (project-current :maybe-prompt))
+ (path (expand-file-name mm-humanwave-icon-component-file
+ (project-root project))))
+ (unless (file-exists-p path)
+ (user-error "File `%s' does not exist." path))
+ (with-current-buffer (find-file-noselect path)
+ (let* ((parser (treesit-parser-create 'typescript))
+ (root-node (treesit-parser-root-node parser))
+ (query `((((lexical_declaration
+ (variable_declarator
+ name: (identifier) @name)) @the_catch)
+ (:equal @name "ICON_MAP"))
+ (((variable_declaration
+ (variable_declarator
+ name: (identifier) @name)) @the_catch)
+ (:equal @name "ICON_MAP"))))
+ (captures (treesit-query-capture root-node query))
+ (found-node (alist-get 'the_catch captures)))
+ found-node))))
+
+(defun mm-humanwave--icon-list (found-node)
+ (let ((captures (treesit-query-capture found-node '((pair) @the_pair)))
+ (pairs nil))
+ (when captures
+ (dolist (capture captures)
+ (let* ((pair-node (cdr capture))
+ (key-node (treesit-node-child-by-field-name pair-node "key"))
+ (val-node (treesit-node-child-by-field-name pair-node "value")))
+ (when (and key-node val-node)
+ (push (cons (mm-camel-to-lisp
+ (treesit-node-text key-node :no-property))
+ (treesit-node-text val-node :no-property))
+ pairs))))
+ (sort pairs :key #'car :lessp #'string<))))
+
+(defun mm-humanwave-insert-icon-component ()
+ "Insert an icon at point with completion.
+
+This command provides completion for the available props that can be
+given to the <icon /> component. The parser searches for the `ICON_MAP'
+definition in the file specified by `mm-humanwave-icon-component-file'."
+ (interactive "" vue-ts-mode)
+ (if-let* ((node (mm-humanwave--find-icon-map))
+ (alist (mm-humanwave--icon-list node)))
+ (let* ((max-key-width
+ (thread-last
+ alist
+ (mapcar (lambda (pair) (length (car pair))))
+ (apply #'max)
+ (+ 4)))
+ (completion-extra-properties
+ `(:annotation-function
+ ,(lambda (key)
+ (concat
+ (propertize " "
+ 'display `(space :align-to ,max-key-width))
+ (propertize (cdr (assoc key alist))
+ 'face 'font-lock-string-face)))))
+ (prompt (format-prompt "Icon" nil))
+ (icon (completing-read prompt alist nil :require-match)))
+ (insert (format "<icon %s />" icon)))
+ (error "Unable to find ICON_MAP definitions")))
+
(provide 'mm-humanwave)
diff --git a/.config/emacs/modules/mm-keybindings.el b/.config/emacs/modules/mm-keybindings.el
index 0df92ce..aae9b4d 100644
--- a/.config/emacs/modules/mm-keybindings.el
+++ b/.config/emacs/modules/mm-keybindings.el
@@ -151,7 +151,7 @@ the first command is remapped to the second command."
(with-eval-after-load 'project
(mm-keymap-set project-prefix-map
"g" #'mm-project-find-regexp
- "G" #'mm-project-or-external-find-regexp)
+ "G" #'project-git-grab)
(when mm-humanwave-p
(mm-keymap-set project-prefix-map
@@ -162,6 +162,11 @@ the first command is remapped to the second command."
(mm-keymap-set eat-semi-char-mode-map
"M-o" #'ace-window)))
+(with-eval-after-load 'minibuffer
+ (when mm-humanwave-p
+ (mm-keymap-set minibuffer-mode-map
+ "C-c m" #'mm-humanwave-insert-last-commit-message)))
+
;;; Display Available Keybindings
diff --git a/.config/emacs/modules/mm-search.el b/.config/emacs/modules/mm-search.el
index 9b1c4c4..3afb77e 100644
--- a/.config/emacs/modules/mm-search.el
+++ b/.config/emacs/modules/mm-search.el
@@ -46,4 +46,10 @@ matching respectively."
(interactive (list (project--read-regexp)))
(mm--project-find-wrapper #'project-or-external-find-regexp regexp))
+
+;;; Grab Integration
+
+(use-package grab
+ :commands (grab git-grab project-grab project-git-grab))
+
(provide 'mm-search)
diff --git a/.config/emacs/site-lisp/grab.el b/.config/emacs/site-lisp/grab.el
new file mode 100644
index 0000000..830d976
--- /dev/null
+++ b/.config/emacs/site-lisp/grab.el
@@ -0,0 +1,211 @@
+;;; grab.el --- Emacs integration for grab -*- lexical-binding: t; -*-
+
+;; Author: Thomas Voss <mail@thomasvoss.com>
+;; Description: TODO
+;; Keywords: matching, tools
+
+;;; Commentary:
+;; TODO
+
+;;; Code:
+
+(require 'ansi-color)
+(require 'compile)
+(require 'project)
+
+(defgroup grab nil
+ "Settings for `grab'."
+ :group 'tools)
+
+(defcustom grab-command "grab"
+ "The base executable for the grab tool."
+ :type 'string)
+
+(defcustom git-grab-command "git-grab"
+ "The base executable for the git-grab tool."
+ :type 'string)
+
+(defcustom grab-command-arguments '("-c" "-Halways")
+ "TODO"
+ :type '(repeat string))
+
+(defcustom git-grab-command-arguments grab-command-arguments
+ "TODO"
+ :type '(repeat string))
+
+(defcustom grab-default-pattern '("x// h//" . 3)
+ "TODO"
+ :type '(choice (cons string natnum)
+ (string)))
+
+(defvar grab--header-regexp
+ "^\\([^:\n]+\\):\\([0-9]+\\):")
+
+(defvar grab-results-mode-map
+ (let ((map (make-sparse-keymap)))
+ (keymap-set map "RET" #'grab-goto-match)
+ (keymap-set map "n" #'grab-next-match)
+ (keymap-set map "p" #'grab-prev-match)
+ map)
+ "Keymap for navigating grab matches.")
+
+(define-derived-mode grab-results-mode compilation-mode "Grab"
+ "TODO"
+ (setq-local compilation-error-regexp-alist nil
+ compilation-error-regexp-alist-alist nil
+ next-error-function #'grab-next-error)
+ (add-hook 'compilation-filter-hook #'ansi-color-compilation-filter nil :local)
+ (font-lock-add-keywords nil
+ `((,grab--header-regexp
+ (1 'compilation-info)
+ (2 'compilation-line-number)))))
+
+
+;;; Process Management
+
+(defun grab--run-process (command-args buffer-name)
+ (let ((cmd-string (mapconcat #'shell-quote-argument command-args " ")))
+ (compilation-start cmd-string
+ #'grab-results-mode
+ (lambda (_) buffer-name))))
+
+
+;;; Navigation & Core Logic
+
+(defun grab--valid-match-p ()
+ "Return non-nil if the current line is a match."
+ (save-excursion
+ (beginning-of-line)
+ (and (looking-at grab--header-regexp)
+ (not (string-prefix-p "Grab started" (match-string 1)))
+ (not (string-prefix-p "Grab finished" (match-string 1))))))
+
+(defun grab-display-match (&optional select-window-p)
+ "Parse current line and display the match in the source buffer."
+ (interactive "P")
+ (save-excursion
+ (beginning-of-line)
+ (if (grab--valid-match-p)
+ (let* ((file (match-string-no-properties 1))
+ (offset (string-to-number (match-string-no-properties 2)))
+ (full-path (expand-file-name file default-directory)))
+ (if (not (file-exists-p full-path))
+ (error "File `%s' does not exist" full-path)
+ (let* ((buffer (find-file-noselect full-path))
+ (window (display-buffer
+ buffer '(display-buffer-reuse-window
+ display-buffer-pop-up-window))))
+ (with-selected-window window
+ (let ((pos (or (byte-to-position (1+ offset)) (1+ offset))))
+ (goto-char pos)
+ (when (fboundp 'pulse-momentary-highlight-one-line)
+ (pulse-momentary-highlight-one-line pos))))
+ (when select-window-p
+ (select-window window)))))
+ (user-error "No match found on the current line"))))
+
+(defun grab-goto-match ()
+ "Go to match on current line and select its window."
+ (interactive)
+ (grab-display-match :select-window-p))
+
+(defun grab--search-forward ()
+ (catch 'found
+ (while (re-search-forward grab--header-regexp nil :noerror)
+ (when (grab--valid-match-p)
+ (throw 'found t)))
+ nil))
+
+(defun grab--search-backward ()
+ (catch 'found
+ (while (re-search-backward grab--header-regexp nil :noerror)
+ (when (grab--valid-match-p)
+ (throw 'found t)))
+ nil))
+
+(defun grab-next-match ()
+ "Move to the next match and display it."
+ (interactive)
+ (forward-line 1)
+ (if (grab--search-forward)
+ (progn
+ (beginning-of-line)
+ (grab-display-match))
+ (forward-line -1)
+ (message "No more matches")))
+
+(defun grab-prev-match ()
+ "Move to the previous match and display it."
+ (interactive)
+ (forward-line -1)
+ (if (grab--search-backward)
+ (progn
+ (beginning-of-line)
+ (grab-display-match))
+ (forward-line 1)
+ (message "No previous matches")))
+
+(defun grab-next-error (&optional arg reset)
+ "TODO"
+ (interactive "p")
+ (when reset
+ (goto-char (point-min)))
+ (let ((direction (if (< arg 0) -1 +1))
+ (count (abs arg)))
+ (dotimes (_ count)
+ (if (> direction 0)
+ (progn
+ (end-of-line)
+ (unless (grab--search-forward)
+ (error "No more matches")))
+ (beginning-of-line)
+ (unless (grab--search-backward)
+ (error "No previous matches"))))
+ (beginning-of-line)
+ (grab-goto-match)))
+
+
+;;; Interactive Commands
+
+;;;###autoload
+(defun grab (pattern)
+ "Run grab with PATTERN in the current directory."
+ (interactive
+ (list (read-string (format-prompt "Grab Pattern" nil) grab-default-pattern)))
+ (grab--run-process
+ (flatten-tree (list grab-command grab-command-arguments pattern))
+ "*grab*"))
+
+;;;###autoload
+(defun git-grab (pattern)
+ "Run git grab with PATTERN in the current directory."
+ (interactive
+ (list (read-string (format-prompt "Grab Pattern" nil) grab-default-pattern)))
+ (grab--run-process
+ (flatten-tree (list git-grab-command git-grab-command-arguments pattern))
+ "*grab*"))
+
+;;;###autoload
+(defun project-grab (pattern)
+ "Run grab with PATTERN at the project root."
+ (interactive
+ (list (read-string (format-prompt "Grab Pattern" nil) grab-default-pattern)))
+ (let* ((project (project-current t))
+ (default-directory (project-root project)))
+ (grab--run-process
+ (flatten-tree (list grab-command grab-command-arguments pattern))
+ "*grab*")))
+
+;;;###autoload
+(defun project-git-grab (pattern)
+ "Run git grab with PATTERN at the project root."
+ (interactive
+ (list (read-string (format-prompt "Grab Pattern" nil) grab-default-pattern)))
+ (let* ((project (project-current t))
+ (default-directory (project-root project)))
+ (grab--run-process
+ (flatten-tree (list git-grab-command git-grab-command-arguments pattern))
+ "*grab*")))
+
+(provide 'grab)
+;;; grab.el ends here