summaryrefslogtreecommitdiff
path: root/.config/emacs
diff options
context:
space:
mode:
Diffstat (limited to '.config/emacs')
-rw-r--r--.config/emacs/init.el32
-rw-r--r--.config/emacs/modules/mm-abbrev.el8
-rw-r--r--.config/emacs/modules/mm-editing.el60
-rw-r--r--.config/emacs/modules/mm-humanwave.el141
-rw-r--r--.config/emacs/modules/mm-modeline.el2
-rw-r--r--.config/emacs/modules/mm-projects.el64
-rw-r--r--.config/emacs/modules/mm-theme.el16
-rw-r--r--.config/emacs/modules/mm-treesit.el5
-rw-r--r--.config/emacs/site-lisp/gh.el22
-rw-r--r--.config/emacs/site-lisp/highlighter.el128
-rw-r--r--.config/emacs/site-lisp/live-jq.el101
-rw-r--r--.config/emacs/templates10
12 files changed, 528 insertions, 61 deletions
diff --git a/.config/emacs/init.el b/.config/emacs/init.el
index 56e8476..eb0f23e 100644
--- a/.config/emacs/init.el
+++ b/.config/emacs/init.el
@@ -8,7 +8,7 @@
;; Lord knows why this needs to be so complicated…
;;
;; The ‘eval’ is required in the case that this file is byte-compiled.
-(if mm-darwin-p
+(if mm-humanwave-p
(eval '(setq inhibit-startup-echo-area-message "thomasvoss"))
(eval '(setq inhibit-startup-echo-area-message "thomas")))
@@ -177,17 +177,17 @@ buffer suppressed."
(echo-keystrokes 0.01) ; 0 disables echoing
(echo-keystrokes-help nil)
(extended-command-suggest-shorter nil)
- (initial-buffer-choice t)
+ (initial-buffer-choice nil)
(initial-scratch-message mm-initial-scratch-message)
(kill-do-not-save-duplicates t)
(large-file-warning-threshold nil)
(make-backup-files nil)
- (mode-require-final-newline nil)
+ (mode-require-final-newline t)
(next-error-recenter '(4)) ; ‘center of window’
(read-extended-command-predicate #'command-completion-default-include-p)
(remote-file-name-inhibit-auto-save t)
(remote-file-name-inhibit-delete-by-moving-to-trash t)
- (require-final-newline nil)
+ (require-final-newline mm-humanwave-p)
(save-interprogram-paste-before-kill t)
(user-full-name "Thomas Voss")
(user-mail-address "mail@thomasvoss.com")
@@ -317,6 +317,28 @@ the buffer without saving it."
(require 'mm-darwin)) ; MacOS
(when mm-lsp-p
(require 'mm-lsp)) ; Language Server Protocol
+(when mm-humanwave-p
+ (require 'mm-humanwave)) ; Humanwave Extras
+
+;; TODO: Put this somewhere reasonable
+
+(defvar mm-placeholder-text "‹XX›")
+
+(defun mm-placeholder-insert ()
+ (interactive)
+ (insert mm-placeholder-text))
+
+(defun mm-placeholder-next ()
+ (interactive)
+ (let ((point (point)))
+ (if (search-forward mm-placeholder-text nil :noerror)
+ (delete-region (match-beginning 0) (match-end 0))
+ (goto-char point)
+ (message "No more placeholders after point."))))
+
+(keymap-global-set "C-c i p" #'mm-placeholder-insert)
+(keymap-global-set "C-c n" #'mm-placeholder-next)
+
;;; Postamble
@@ -324,4 +346,4 @@ the buffer without saving it."
(add-hook 'after-init-hook
(defun mm-echo-init-time ()
(message (emacs-init-time "Emacs initialized in %.2f seconds")))
- 100) \ No newline at end of file
+ 100)
diff --git a/.config/emacs/modules/mm-abbrev.el b/.config/emacs/modules/mm-abbrev.el
index 937d558..66ca8d7 100644
--- a/.config/emacs/modules/mm-abbrev.el
+++ b/.config/emacs/modules/mm-abbrev.el
@@ -78,6 +78,14 @@ case-sensitive to avoid unexpected abbreviation expansions."
"sme" "save-mark-and-excursion"
"sr" "save-restriction")
+(when mm-humanwave-p
+ (with-eval-after-load 'python-ts-mode
+ (mm-define-abbreviations python-ts-mode-abbrev-table
+ "empb" "with emphasize.Block():"
+ "empf" "@emphasize.func"
+ "empi" "from shared.system import emphasize"
+ "empt" "emphasize.this")))
+
;;; Template Configuration
diff --git a/.config/emacs/modules/mm-editing.el b/.config/emacs/modules/mm-editing.el
index 540bae4..6054f5a 100644
--- a/.config/emacs/modules/mm-editing.el
+++ b/.config/emacs/modules/mm-editing.el
@@ -56,6 +56,7 @@
(go-ts-mode . (:extras go-ts-mode-indent-offset))
(gsp-ts-mode . (:width 2 :extras gsp-ts-mode-indent-rules))
(helpful-mode . (:width 8)) ; GNU code uses 8-column tabs
+ (json-ts-mode . (:extras json-ts-mode-indent-offset))
(latex-mode . (:width 2))
(lisp-data-mode . (:spaces t))
(lisp-interaction-mode . (:spaces t))
@@ -66,6 +67,7 @@
(python-ts-mode . (:extras python-indent-offset))
(sgml-mode . (:extras sgml-basic-offset))
(sh-mode . (:extras sh-basic-offset))
+ (sql-mode . (:extras sqlind-basic-offset))
(tex-mode . (:width 2))
(typescript-ts-mode . (:extras typescript-ts-mode-indent-offset))
(vimscript-ts-mode . (:extras vimscript-ts-mode-indent-level))
@@ -125,6 +127,11 @@ those should be listed in `mm-editing-indentation-settings'."
extras))
(set (make-local-variable extra) tabsize)))))
+(use-package sh-mode
+ :custom
+ (sh-indent-for-case-label 0)
+ (sh-indent-for-case-alt #'+))
+
;;; Code Commenting
@@ -298,6 +305,18 @@ surround with spaces."
(push '("`" . "'") surround-pairs))))
+;;; Insert Webpage Contents
+
+(defun mm-insert-from-url (url)
+ "Insert the contents of URL at point."
+ (interactive
+ (let ((url-at-point (thing-at-point 'url)))
+ (list (read-string
+ (format-prompt "URL" url-at-point)
+ nil nil url-at-point))))
+ (call-process "curl" nil '(t nil) nil url))
+
+
;;; Emmet Mode
(defun mm-editing-emmet-dwim (arg)
@@ -318,6 +337,36 @@ is as described by `emmet-expand-line'."
(emmet-self-closing-tag-style ""))
+;;; JQ Manipulation in JSON Mode
+
+(defun mm-jq-filter (query &optional beg end)
+ "TODO"
+ (interactive
+ (list
+ (read-string (format-prompt "Query" nil))
+ (when (use-region-p) (region-beginning))
+ (when (use-region-p) (region-end))))
+ (let* ((beg (or beg (point-min)))
+ (end (or end (point-max)))
+ (temp-buffer (generate-new-buffer "* jq temp*"))
+ (exit-code (call-process-region beg end "jq" nil temp-buffer nil
+ "--tab" query))
+ (output (with-current-buffer temp-buffer (buffer-string))))
+ (if (zerop exit-code)
+ (atomic-change-group
+ (delete-region beg end)
+ (insert output)
+ (indent-region beg (point)))
+ (message "%s" output))
+ (kill-buffer temp-buffer)))
+
+(use-package json-ts-mode
+ :bind ( :map json-ts-mode-map
+ ("C-|" . #'mm-jq-filter))
+ :config
+ (require 'live-jq))
+
+
;;; Number Formatting
(use-package number-format-mode
@@ -329,6 +378,7 @@ is as described by `emmet-expand-line'."
;;; Additional Major Modes
(use-package awk-ts-mode :ensure t)
+(use-package cmake-mode :ensure t)
(use-package git-modes :ensure t)
(use-package po-mode :ensure t)
(use-package sed-mode :ensure t)
@@ -362,6 +412,12 @@ is as described by `emmet-expand-line'."
;;; Add Missing Extensions
(dolist (pattern '("\\.tmac\\'" "\\.mom\\'"))
- (add-to-list 'auto-mode-alist (cons pattern 'nroff-mode)))
+ (add-to-list 'auto-mode-alist (cons pattern #'nroff-mode)))
+
+
+;;; Subword Navigation
+
+(use-package subword
+ :hook prog-mode)
-(provide 'mm-editing) \ No newline at end of file
+(provide 'mm-editing)
diff --git a/.config/emacs/modules/mm-humanwave.el b/.config/emacs/modules/mm-humanwave.el
new file mode 100644
index 0000000..3aa97b3
--- /dev/null
+++ b/.config/emacs/modules/mm-humanwave.el
@@ -0,0 +1,141 @@
+;;; mm-humanwave.el --- Humanwave extras -*- lexical-binding: t; -*-
+
+
+;;; Query the Backend
+
+(defvar mm--humanwave-query-history nil
+ "History for endpoints given to `mm-humanwave-query'.")
+
+(defun mm-humanwave-query (endpoint &optional method)
+ "Query and display the result of an HTTP reqiest on ENDPOINT.
+If METHOD is nil, a GET request is performed."
+ (interactive
+ (let* ((query (read-string (format-prompt "Query" nil)
+ (car-safe mm--humanwave-query-history)
+ 'mm--humanwave-query-history))
+ (parts (string-split (string-trim query) " " :omit-nulls)))
+ (when (length> parts 2)
+ (user-error "Queries must be of the form `METHOD ENDPOINT' or `ENDPOINT'."))
+ (nreverse parts)))
+ (let* ((project-root (project-root (project-current :maybe-prompt)))
+ (qry-path (expand-file-name "qry" project-root))
+ extras)
+ (unless (file-executable-p qry-path)
+ (user-error "No `qry' executable found in the project root"))
+ (let ((output-buffer (get-buffer-create "*Query Response*")))
+ (with-current-buffer output-buffer
+ (delete-region (point-min) (point-max))
+ (call-process qry-path nil t nil
+ (string-trim endpoint) "-X" (or method "GET"))
+ (unless (eq major-mode 'json-ts-mode)
+ (json-ts-mode))
+ (goto-char (point-min)))
+ (display-buffer output-buffer))))
+
+(keymap-set project-prefix-map "q" #'mm-humanwave-query)
+
+
+;;; IMenu Support for Handlers
+
+(defvar mm-humanwave-handler--regexp
+ (rx bol
+ (* blank)
+ (or "if" "elif")
+ (* blank)
+ (or "topic" "schedule")
+ (* blank)
+ "=="
+ (* blank)
+ (or (seq ?\' (* (not ?\')) ?\')
+ (seq ?\" (* (not ?\")) ?\"))
+ (* blank)
+ ?:
+ (* blank)
+ eol))
+
+(defun mm-humanwave-handler-topic-imenu-index ()
+ (let ((tree-index (when (fboundp 'treesit-simple-imenu--generic-function)
+ (treesit-simple-imenu--generic-function
+ treesit-simple-imenu-settings)))
+ (topic-index '()))
+ (save-excursion
+ (goto-char (point-min))
+ (while (re-search-forward mm-humanwave-handler--regexp nil :noerror)
+ (let ((label (match-string 0))
+ (pos (match-beginning 0)))
+ (push (cons (format "Topic: %s" (string-trim label)) pos) topic-index))))
+ (append (nreverse topic-index) tree-index)))
+
+(defun mm-humanwave-handler-topic-imenu-setup ()
+ "Setup custom imenu index for `python-ts-mode'."
+ (when (and (string-match-p "handlers?" (or (buffer-file-name) ""))
+ (derived-mode-p #'python-ts-mode))
+ (setq-local imenu-create-index-function
+ #'mm-humanwave-handler-topic-imenu-index)))
+
+(add-hook 'after-change-major-mode-hook
+ #'mm-humanwave-handler-topic-imenu-setup)
+
+
+;;; Insert Imports in Vue
+
+(defun mm-humanwave-insert-vue-import-path (base-directory target-file)
+ "Insert an import directive at POINT.
+The import directive imports TARGET-FILE relative from BASE-DIRECTORY.
+When called interactively BASE-DIRECTORY is the directory of the current
+open Vue file and TARGET-FILE is a file in the current project that is
+queried interactively.
+
+When called interactively the prefix argument can be used to emulate the
+behaviour of the INCLUDE-ALL-P argument to `mm-project-read-file-name'."
+ (interactive
+ (list
+ default-directory
+ (mm-humanwave-project-read-file-name current-prefix-arg)))
+ (let ((path (file-name-sans-extension
+ (file-relative-name target-file base-directory))))
+ (unless (string-match-p "/" path)
+ (setq path (concat "./" path)))
+ (insert "import ")
+ (save-excursion
+ (insert (thread-last
+ (file-name-base path)
+ (mm-string-split "-")
+ (mapconcat #'capitalize)))
+ (push-mark (point))
+ (insert (format " from '%s';" path)))))
+
+(defun mm-humanwave-project-read-file-name (&optional include-all-p)
+ "Prompt for a project file.
+This function is similar to `project-find-file', but it returns the path
+to the selected file instead of opening it.
+
+When called interactively the selected file is printed to the minibuffer,
+otherwise it is returned.
+
+The prefix argument INCLUDE-ALL-P is the same as the INCLUDE-ALL argument
+to the `project-find-file' command."
+ (interactive "P")
+ (let* ((project (project-current :maybe-prompt))
+ (root (project-root project))
+ (files (if include-all-p
+ (let ((vc-ignores (mapcar
+ (lambda (dir) (concat dir "/"))
+ vc-directory-exclusion-list)))
+ (project--files-in-directory root vc-ignores))
+ (project-files project)))
+ (canditates (mapcar (lambda (f) (file-relative-name f root))
+ files))
+ (table (lambda (string predicate action)
+ (if (eq action 'metadata)
+ '(metadata (category . file))
+ (complete-with-action action canditates string predicate))))
+ (default-directory root)
+ (choice (completing-read (format-prompt "Find project file" nil)
+ table nil :require-match)))
+ (let ((path (expand-file-name choice root)))
+ (if (called-interactively-p 'any)
+ (message "%s" path)
+ path))))
+
+(provide 'mm-humanwave)
diff --git a/.config/emacs/modules/mm-modeline.el b/.config/emacs/modules/mm-modeline.el
index d1ec6f0..44fc115 100644
--- a/.config/emacs/modules/mm-modeline.el
+++ b/.config/emacs/modules/mm-modeline.el
@@ -90,7 +90,7 @@ the text it should be mapped to.")
((derived-mode-p 'comint-mode) "$ ")
((derived-mode-p 'conf-mode) "# ")
((derived-mode-p 'prog-mode) "λ ")
- ((derived-mode-p 'special-mode) "❇ ")
+ ((derived-mode-p 'special-mode) "★ ")
((derived-mode-p 'text-mode) "§ ")
(:default ""))
'face 'mm-modeline-major-mode-symbol-face))
diff --git a/.config/emacs/modules/mm-projects.el b/.config/emacs/modules/mm-projects.el
index 71006d5..02d7af4 100644
--- a/.config/emacs/modules/mm-projects.el
+++ b/.config/emacs/modules/mm-projects.el
@@ -19,7 +19,7 @@ This is intended to be called interactively via
(project-find-file "Find File" ?f)
(mm-projects-project-magit-status "Git Status" ?s)))
:config
- (unless mm-darwin-p
+ (unless mm-humanwave-p
(if-let ((repo-directory (getenv "REPODIR")))
(let* ((list-dir
(lambda (path)
@@ -47,9 +47,10 @@ This is intended to be called interactively via
(use-package magit
:ensure t
- :bind ( :map magit-status-mode-map
- ("[" . magit-section-backward-sibling)
- ("]" . magit-section-forward-sibling))
+ :bind (("C-c b" . magit-blame-addition)
+ :map magit-status-mode-map
+ ("[" . magit-section-backward-sibling)
+ ("]" . magit-section-forward-sibling))
:custom
(git-commit-style-convention-checks
'(non-empty-second-line overlong-summary-line))
@@ -70,11 +71,18 @@ This is intended to be called interactively via
"push" "-v" args remote
(format "refs/heads/%s:refs/heads/%s" branch branch)))))
(transient-append-suffix #'magit-push '(1 -1)
- '("a" "all remotes" mm-projects-magit-push-current-to-all-remotes)))
+ '("a" "all remotes" mm-projects-magit-push-current-to-all-remotes))
+ (add-to-list 'magit-blame-styles
+ '(margin
+ (show-lines . t)
+ (margin-format . (" %C %a" " %s%f" " "))
+ (margin-width . 73)
+ (margin-face . magit-blame-margin)
+ (margin-body-face . (magit-blame-dimmed)))))
(use-package magit-repos
:ensure nil ; Part of ‘magit’
- :if (not mm-darwin-p)
+ :if (not mm-humanwave-p)
:commands (magit-list-repositories)
:init
(if-let ((directory (getenv "REPODIR")))
@@ -100,45 +108,7 @@ This is intended to be called interactively via
;;; GitHub Pull Requests
(require 'gh)
-(keymap-global-set "C-c p" #'gh-create-pr)
+(keymap-global-set "C-c p c" #'gh-create-pr)
+(keymap-global-set "C-c p o" #'gh-open-previous-pr)
-;; (defun mm-gh--get-labels ()
-;; (with-temp-buffer
-;; (call-process "gh" nil t nil "label" "list" "--json" "name")
-;; (goto-char (point-min))
-;; (let* ((data (json-parse-buffer))
-;; (labels (seq-map (lambda (x) (gethash "name" x)) data)))
-;; (sort labels
-;; :in-place t
-;; :lessp (lambda (x y)
-;; (let ((prefix-x-p (string-prefix-p "Sprint " x))
-;; (prefix-y-p (string-prefix-p "Sprint " y)))
-;; (cond
-;; ((and prefix-x-p prefix-y-p) (string> x y))
-;; (prefix-x-p t)
-;; (prefix-y-p nil)
-;; (:else (string< x y)))))))))
-
-;; (defun mm-gh-create-pr (title draftp labels)
-;; "Create a GitHub pull request using the gh CLI.
-;; If DRAFT is non-nil, the PR will be created as a draft.
-;; LABELS should be a comma-separated string of GitHub labels."
-;; (interactive
-;; (list
-;; (read-string (format-prompt "PR Title" nil))
-;; (y-or-n-p "Create as draft PR? ")
-;; (completing-read-multiple (format-prompt "PR Labels" nil)
-;; (mm-gh--get-labels))))
-;; (let* ((branch (car (vc-git-branches)))
-;; (title (format "%s %s" branch title))
-;; (flags `("--fill-verbose" "--title" ,title "--assignee" "@me"))
-;; (label-string (mapconcat #'identity labels ",")))
-;; (when draftp
-;; (setq flags (append flags '("--draft"))))
-;; (when labels
-;; (setq flags (append flags `("--label" ,label-string))))
-;; (with-temp-buffer
-;; (apply #'call-process "gh" nil t nil "pr" "create" flags)
-;; (message (buffer-string)))))
-
-(provide 'mm-projects) \ No newline at end of file
+(provide 'mm-projects)
diff --git a/.config/emacs/modules/mm-theme.el b/.config/emacs/modules/mm-theme.el
index 90b89ba..cde214a 100644
--- a/.config/emacs/modules/mm-theme.el
+++ b/.config/emacs/modules/mm-theme.el
@@ -8,7 +8,7 @@
;;; Fonts
-(defvar mm-theme-monospace-font `(,(if mm-darwin-p
+(defvar mm-theme-monospace-font `(,(if mm-humanwave-p
"Iosevka Custom"
"Iosevka Smooth")
:weight regular
@@ -166,6 +166,16 @@ See also the `mm-theme-background-opacity' variable."
(("C-c h l" . pulsar-highlight-dwim)))
+;;; In-buffer highlighting
+
+(require 'hi-lock) ; For extra face definitions
+(use-package highlighter
+ :bind (("C-c h m" . #'highlighter-mark)
+ ("C-c h u" . #'highlighter-unmark)
+ ("C-c h U" . #'highlighter-unmark-buffer))
+ :commands (highlighter-mark highlighter-unmark highlighter-unmark-buffer))
+
+
;;; Add Padding
(use-package spacious-padding
@@ -224,7 +234,7 @@ See also the `mm-theme-background-opacity' variable."
;;; Indent Guides
-(when mm-darwin-p
+(when mm-humanwave-p
(use-package highlight-indent-guides
:ensure t
:hook ((jinja2-mode vue-ts-mode mhtml-mode) . highlight-indent-guides-mode)
@@ -233,4 +243,4 @@ See also the `mm-theme-background-opacity' variable."
(highlight-indent-guides-auto-even-face-perc 30)
(highlight-indent-guides-auto-odd-face-perc 0)))
-(provide 'mm-theme) \ No newline at end of file
+(provide 'mm-theme)
diff --git a/.config/emacs/modules/mm-treesit.el b/.config/emacs/modules/mm-treesit.el
index 1d4defa..bd146ce 100644
--- a/.config/emacs/modules/mm-treesit.el
+++ b/.config/emacs/modules/mm-treesit.el
@@ -35,6 +35,8 @@
"https://github.com/tree-sitter/tree-sitter-java")
(javascript
"https://github.com/tree-sitter/tree-sitter-javascript")
+ (json
+ "https://github.com/tree-sitter/tree-sitter-json")
(markdown
"https://github.com/tree-sitter-grammars/tree-sitter-markdown"
"split_parser" "tree-sitter-markdown/src")
@@ -116,6 +118,7 @@ The parsers are taken from `treesit-language-source-alist'."
(defvar mm-treesit-language-file-name-alist
'((go . "\\.go\\'")
(gomod . "/go\\.mod\\'")
+ (json . "\\.json\\'")
(tsx . "\\.tsx\\'")
(typescript . "\\.ts\\'"))
"Alist mapping languages to their associated file-names.
@@ -219,4 +222,4 @@ back to regular `expreg-expand'."
:commands (mm-expreg-expand mm-expreg-expand-dwim)
:bind ("M-SPC" . mm-expreg-expand-dwim))
-(provide 'mm-treesit) \ No newline at end of file
+(provide 'mm-treesit)
diff --git a/.config/emacs/site-lisp/gh.el b/.config/emacs/site-lisp/gh.el
index 0461b18..23086e5 100644
--- a/.config/emacs/site-lisp/gh.el
+++ b/.config/emacs/site-lisp/gh.el
@@ -36,4 +36,24 @@ via `gh-get-labels'."
(apply #'call-process "gh" nil t nil "pr" "create" flags)
(message (buffer-string)))))
-(provide 'gh) \ No newline at end of file
+(defvar gh-pr-regexp
+ "\\`https://\\(?:www\\.\\)?github\\.com/[^/]+/[^/]+/pull/[[:digit:]]+\\'")
+
+(defun gh--pr-link-p (s)
+ (declare (pure t) (side-effect-free t))
+ (string-match-p gh-pr-regexp s))
+
+(defun gh-open-previous-pr ()
+ "Open the previous GitHub pull request.
+Opens the previous pull request created by `gh-create-pr' by searching
+for the echoed URL in the `*Messages*' buffer."
+ (interactive)
+ (with-current-buffer "*Messages*"
+ (goto-char (point-max))
+ (while (not (gh--pr-link-p (buffer-substring-no-properties
+ (pos-bol) (pos-eol))))
+ (unless (line-move -1 :noerror)
+ (user-error "No previous pull request found.")))
+ (browse-url-at-point)))
+
+(provide 'gh)
diff --git a/.config/emacs/site-lisp/highlighter.el b/.config/emacs/site-lisp/highlighter.el
new file mode 100644
index 0000000..ce67ac8
--- /dev/null
+++ b/.config/emacs/site-lisp/highlighter.el
@@ -0,0 +1,128 @@
+;;; highlighter.el --- In-buffer highlighting commands -*- lexical-binding: t; -*-
+
+(require 'seq)
+
+(defgroup highlighter nil
+ "Customization group for `highlighter'."
+ :group 'convenience)
+
+(defcustom highlighter-default-face 'match
+ "The default face used by `highlighter-mark'."
+ :type 'face
+ :package-version '(highlighter . "1.0.0")
+ :group 'highlighter)
+
+(defun highlighter-mark (arg)
+ "Highlight text in the buffer.
+Highlight the current line or region if it is active. Text is
+highlighted using the face specified by `highlighter-default-face'.
+
+With ARG, interactively pick a face to highlight with."
+ (declare (interactive-only t))
+ (interactive "P")
+ (let ((bounds (if (use-region-p)
+ (region-bounds)
+ `((,(pos-bol) . ,(pos-eol)))))
+ (face (when arg
+ (highlighter--read-face-name "Highlight with face" #'facep))))
+ (highlighter-mark-region bounds face))
+ (when (region-active-p)
+ (deactivate-mark)))
+
+(defun highlighter-unmark (arg)
+ "Remove highlights in the buffer.
+
+Remove highlights from the current line or region if it is active.
+
+With ARG, interactively pick a face. Only highlights using the chosen
+face will be removed."
+ (declare (interactive-only t))
+ (interactive "P")
+ (let ((bounds (if (use-region-p)
+ (region-bounds)
+ `((,(pos-bol) . ,(pos-eol)))))
+ (face (when arg
+ (highlighter--read-face-name
+ "Clear highlights using face"
+ #'highlighter--used-face-p))))
+ (highlighter-unmark-region bounds face))
+ (when (region-active-p)
+ (deactivate-mark)))
+
+(defun highlighter-mark-region (bounds &optional face)
+ "Highlight text in the buffer within BOUNDS.
+BOUNDS uses the same format as returned by `region-bounds'.
+
+Text is highlighted using the face specified by `highlighter-default-face'.
+
+If FACE is nil or omitted, `highlighter-default-face' is used."
+ (dolist (x bounds) (highlighter--mark-region (car x) (cdr x) face)))
+
+(defun highlighter-unmark-region (bounds &optional face)
+ "Remove highlights in the buffer within BOUNDS.
+BOUNDS uses the same format as returned by `region-bounds'.
+
+If FACE is non-nil, only remove highlights using FACE."
+ (dolist (x bounds) (highlighter--unmark-region (car x) (cdr x) face)))
+
+(defun highlighter--mark-region (beg end &optional face)
+ (let ((ov (make-overlay beg end nil :front-advance))
+ (face (or face highlighter-default-face 'match)))
+ (overlay-put ov 'priority 1)
+ (overlay-put ov 'face face)
+ (overlay-put ov 'evaporate t)
+ (overlay-put ov 'highlighter--mark-p t)
+ (overlay-put ov 'highlighter--face face)))
+
+(defun highlighter--unmark-region (beg end &optional face)
+ (if face
+ (remove-overlays beg end 'highlighter--face face)
+ (remove-overlays beg end 'highlighter--mark-p t)))
+
+(defun highlighter-unmark-buffer (arg)
+ "Remove highlights in the buffer.
+
+With ARG, interactively pick a face. Only highlights using the chosen
+face will be removed."
+ (declare (interactive-only t))
+ (interactive "P")
+ (let ((face (when arg
+ (highlighter--read-face-name
+ "Clear highlights using face"
+ #'highlighter--used-face-p))))
+ (highlighter--unmark-region (point-min) (point-max) face)))
+
+(defun highlighter--read-face-name (prompt face-predicate)
+ (let (default defaults)
+ (let ((prompt (format "%s: " prompt))
+ (completion-extra-properties
+ `(:affixation-function
+ ,(lambda (faces)
+ (mapcar
+ (lambda (face)
+ (list face
+ (concat (propertize read-face-name-sample-text
+ 'face face)
+ "\t")
+ ""))
+ faces))))
+ aliasfaces nonaliasfaces faces)
+ ;; Build up the completion tables.
+ (mapatoms (lambda (s)
+ (when (apply face-predicate s nil)
+ (if (get s 'face-alias)
+ (push (symbol-name s) aliasfaces)
+ (push (symbol-name s) nonaliasfaces)))))
+ (let ((face (completing-read
+ prompt
+ (completion-table-in-turn nonaliasfaces aliasfaces)
+ nil t nil 'face-name-history defaults)))
+ (when (facep face) (if (stringp face)
+ (intern face)
+ face))))))
+
+(defun highlighter--used-face-p (face)
+ (seq-filter (lambda (ov) (eq face (overlay-get ov 'highlighter--face)))
+ (overlays-in (point-min) (point-max))))
+
+(provide 'highlighter)
diff --git a/.config/emacs/site-lisp/live-jq.el b/.config/emacs/site-lisp/live-jq.el
new file mode 100644
index 0000000..f8a0a7f
--- /dev/null
+++ b/.config/emacs/site-lisp/live-jq.el
@@ -0,0 +1,101 @@
+;; TODO: ‘defcustom’ this
+(defvar live-jq-major-mode
+ (cond ((fboundp #'json-ts-mode) #'json-ts-mode)
+ ((fboundp #'json-mode) #'json-mode))
+ "TODO")
+
+(defvar live-jq--input-buffer nil
+ "The buffer containing the original JSON data.")
+
+(defvar live-jq--preview-buffer "*JQ Preview*"
+ "The buffer showing the live jq results.")
+
+(defvar live-jq--last-query "")
+
+(defun live-jq--get-json-input ()
+ "Return the contents of the input buffer as a string."
+ (with-current-buffer live-jq--input-buffer
+ (buffer-substring-no-properties (point-min) (point-max))))
+
+(defun live-jq--run-jq (query)
+ "Run jq QUERY on the input buffer's content and return result string or nil on error."
+ (let ((json-input (live-jq--get-json-input)))
+ (with-temp-buffer
+ (insert json-input)
+ (let ((exit-code (call-process-region
+ (point-min) (point-max)
+ "jq" :delete t nil "--tab" query)))
+ (when (zerop exit-code)
+ (buffer-string))))))
+
+(defun live-jq--render-jq-preview (query)
+ "Update preview buffer with the result or error of jq QUERY."
+ (let* ((preview-buffer (get-buffer-create live-jq--preview-buffer))
+ (json-input (live-jq--get-json-input))
+ (inhibit-read-only t))
+ (with-current-buffer preview-buffer
+ (erase-buffer)
+ (condition-case err
+ (with-temp-buffer
+ (insert json-input)
+ (let ((exit-code (call-process-region
+ (point-min) (point-max)
+ "jq" nil preview-buffer nil "--tab" query)))
+ (when (not (zerop exit-code))
+ (erase-buffer)
+ (insert "%s\n%s"
+ (propertize (format "jq error (exit %d): %s" exit-code query)
+ 'face 'error)
+ json-input))))
+ (error
+ (insert "%s\n%s"
+ (propertize (format "Error: %s" err) 'face 'error)
+ input-json)))
+ (goto-char (point-min))
+ (when live-jq-major-mode
+ (funcall live-jq-major-mode))))
+ (display-buffer live-jq--preview-buffer))
+
+(defun live-jq--minibuffer-update ()
+ "Update preview as user types."
+ (let ((query (minibuffer-contents)))
+ (unless (equal query live-jq--last-query)
+ (setq live-jq--last-query query)
+ (live-jq--render-jq-preview query))))
+
+;;;###autoload
+(defun live-jq ()
+ "Prompt for a jq query, show live results, and replace buffer on confirmation."
+ (interactive)
+ (unless (executable-find "jq")
+ (user-error "`jq' not found in PATH."))
+
+ (setq live-jq--input-buffer (current-buffer))
+ (setq live-jq--last-query "")
+
+ ;; Clean up preview buffer if user cancels with C-g
+ (let ((minibuffer-setup-hook
+ (list (lambda ()
+ ;; Add post-command-hook for live preview
+ (add-hook 'post-command-hook #'live-jq--minibuffer-update nil t)
+ ;; Add abort cleanup
+ (add-hook 'minibuffer-exit-hook
+ (lambda ()
+ (when (get-buffer live-jq--preview-buffer)
+ (kill-buffer live-jq--preview-buffer)))
+ nil t)))))
+ (let ((query (read-from-minibuffer (format-prompt "Query" nil))))
+ (unwind-protect
+ (let ((result (live-jq--run-jq query)))
+ (if result
+ (with-current-buffer live-jq--input-buffer
+ (let ((inhibit-read-only t))
+ (erase-buffer)
+ (insert result))
+ (message "jq applied."))
+ (user-error "Invalid jq query: see *jq-preview* for details")))
+ ;; Cleanup preview buffer after any outcome
+ (when (get-buffer live-jq--preview-buffer)
+ (kill-buffer live-jq--preview-buffer))))))
+
+(provide 'live-jq)
diff --git a/.config/emacs/templates b/.config/emacs/templates
index 6749183..ad4e97a 100644
--- a/.config/emacs/templates
+++ b/.config/emacs/templates
@@ -27,4 +27,12 @@ emacs-lisp-mode
(buffer-name)))
" --- " p " -*- lexical-binding: t; -*-" n n q)
-(section "\f" n ";;; " p n n q) \ No newline at end of file
+(section "\f" n ";;; " p n n q)
+
+vue-ts-mode
+
+(fcrow
+ > "<div class=\"fc-row\">"
+ n> "<label>{{ _('" p "') }}</label>"
+ n> q
+ n> "</div>") \ No newline at end of file