diff options
| author | Thomas Voss <mail@thomasvoss.com> | 2026-03-22 17:05:21 +0100 |
|---|---|---|
| committer | Thomas Voss <mail@thomasvoss.com> | 2026-03-22 17:05:21 +0100 |
| commit | f0e047b0674817c3fba38386a151c0b2ed620f50 (patch) | |
| tree | 38598c348f7f536c2f0c302693856349639295d6 /.config/emacs | |
| parent | 9a40ca3f9f198cb14c64c25edcf7c325ca2b8e5e (diff) | |
| parent | cb6831070008c56d3f287c4c754526d2c9d735a2 (diff) | |
Diffstat (limited to '.config/emacs')
| -rw-r--r-- | .config/emacs/init.el | 9 | ||||
| -rw-r--r-- | .config/emacs/mango-theme.el | 6 | ||||
| -rw-r--r-- | .config/emacs/modules/mm-completion.el | 99 | ||||
| -rw-r--r-- | .config/emacs/modules/mm-editing.el | 17 | ||||
| -rw-r--r-- | .config/emacs/modules/mm-humanwave.el | 93 | ||||
| -rw-r--r-- | .config/emacs/modules/mm-keybindings.el | 7 | ||||
| -rw-r--r-- | .config/emacs/modules/mm-search.el | 6 | ||||
| -rw-r--r-- | .config/emacs/site-lisp/grab.el | 211 |
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 |