hamacs/ha-programming.org
Howard Abrams 203ebcc652 Get write-good to parse Markdown better
Using remark to parse an Markdown file, and then made a plugin for it
that uses unist-util to strip out code blocks, and pass that to
write-good.

Why the trouble? My new team uses `just` instead of `make`, and that
is a _weasel word_ for write-good. Now, when `just` is written with
backticks, write-good no longer flags it.
2026-08-06 10:26:59 -07:00

1516 lines
70 KiB
Org Mode
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#+title: General Programming Configuration
#+author: Howard X. Abrams
#+date: 2020-10-26
#+tags: emacs programming yaml ansible docker json
A literate programming file for helping me program.
#+begin_src emacs-lisp :exports none
;;; general-programming --- Configuration for general languages. -*- lexical-binding: t; -*-
;;
;; © 2020-2023 Howard X. Abrams
;; Licensed under a Creative Commons Attribution 4.0 International License.
;; See http://creativecommons.org/licenses/by/4.0/
;;
;; Author: Howard X. Abrams <http://gitlab.com/howardabrams>
;; Maintainer: Howard X. Abrams
;; Created: October 26, 2020
;;
;; This file is not part of GNU Emacs.
;;
;; *NB:* Do not edit this file. Instead, edit the original literate file at:
;; ~/src/hamacs/ha-programming.org
;; And tangle the file to recreate this one.
;;
;;; Code:
#+end_src
* Introduction
Configuration for programming interfaces and workflows that behave similarly.
* General
The following work for all programming languages.
** Virtual Environments with Mise
The [[https://github.com/eki3z/mise.el][mise.el]] project overcomes the [[https://mise.jdx.dev/ide-integration.html#emacs][need for shims]] and other hoops when working with [[https://mise.jdx.dev/walkthrough.html][Mise]].
#+BEGIN_SRC emacs-lisp
(use-package mise
:straight (:type git :host github :repo "eki3z/mise.el")
:hook (after-init . global-mise-mode))
#+END_SRC
Now, per project, create a =mise.toml= that contains tools, environment variables, etc.
#+BEGIN_SRC toml
[env]
AWS_DEFAULT_REGION = "us-gov-east-1"
[tools]
python = {version='3', virtualenv='~/.venv/govcloud_lambda'}
#+END_SRC
Or create the entries by using CLI commands:
#+BEGIN_SRC sh :tangle no
mise set AWS_DEFAULT_REGION=us-gov-east-1
#+END_SRC
** Virtual Environments with direnv
Farm off commands into /virtual environments/:
#+begin_src emacs-lisp
(use-package direnv
:init
(setq direnv-always-show-summary t
direnv-show-paths-in-summary t)
(if (file-exists-p "/opt/homebrew")
(setq direnv--executable "/opt/homebrew/bin/direnv")
(setq direnv--executable "/usr/local/bin/direnv"))
:config
(direnv-mode))
#+end_src
** Displaying Code
*** Displaying Line Numbers
For all programming languages, I would like to now default to absolute line numbers, and turn off the [[file:ha-display.org::*Mixed Pitch][Mixed Pitch]] feature that seems to /come along/ for the ride:
#+begin_src emacs-lisp
(use-package emacs
:config
(defun ha-prog-mode-config ()
"Configure the `prog-mode'"
(setq display-line-numbers t))
:hook
(prog-mode . ha-prog-mode-config))
#+end_src
*** Code Folding
While Emacs has options for viewing and moving around code, sometimes, we could /collapse/ all functions, and then start to expand them one at a time. For this, we could enable the built-in [[https://www.emacswiki.org/emacs/HideShow][hide-show feature]]:
#+begin_src emacs-lisp :tangle no
(use-package hide-show
:straight (:type built-in)
:init
(setq hs-hide-comments t
hs-hide-initial-comment-block t
hs-isearch-open t)
:hook (prog-mode . hs-minor-mode))
#+end_src
Note that =hide-show= doesnt work with complex YAML files. The [[https://github.com/gregsexton/origami.el][origami]] mode works better /out-of-the-box/, as it works with Python and Lisp, but falls back to indents as the format, which works well.
#+begin_src emacs-lisp
(use-package origami
:init
(setq origami-fold-replacement "")
:hook (prog-mode . origami-mode))
#+end_src
To take advantage of this, type:
- ~z m~ :: To collapse everything
- ~z r~ :: To open everything
- ~z o~ :: To open a particular section
- ~z c~ :: To collapse a /section/ (like a function)
- ~z a~ :: Toggles open to close
Note: Yes, we could use [[https://github.com/mrkkrp/vimish-fold][vimish-fold]] (and its cousin, [[https://github.com/alexmurray/evil-vimish-fold][evil-vimish-fold]]) and well see if I need those.
*** Smart Parenthesis
We need to make sure we keep the [[https://github.com/Fuco1/smartparens][smartparens]] project always in /strict mode/, because who wants to worry about paren-matching:
#+begin_src emacs-lisp
(use-package smartparens
:custom
(smartparens-global-strict-mode t)
:config
(sp-with-modes sp-lisp-modes
;; disable ', as it's the quote character:
(sp-local-pair "'" nil :actions nil))
(sp-with-modes (-difference sp-lisp-modes sp-clojure-modes)
;; use the pseudo-quote inside strings where it serve as hyperlink.
(sp-local-pair "`" "'"
:when '(sp-in-string-p
sp-in-comment-p)
:skip-match (lambda (ms _mb _me)
(cond
((equal ms "'") (not (sp-point-in-string-or-comment)))
(t (not (sp-point-in-string-or-comment)))))))
:hook
(prog-mode . smartparens-strict-mode))
#+end_src
** Symbol Highlighting
I appreciate calling =hi-lock-face-symbol-at-point= (or =highlight-symbol-at-point=) to highlight all instances of a variable or other symbol.
#+begin_src emacs-lisp
(use-package auto-highlight-symbol
:config
(setq ahs-idle-interval 0.1)
(set-face-attribute ahs-face nil :foreground 'unspecified :background 'unspecified
:weight 'ultra-bold :slant 'italic)
(set-face-attribute ahs-plugin-default-face nil :foreground 'unspecified
:background 'unspecified :weight 'bold :slant 'normal))
#+end_src
Instead of calling =global-auto-highlight-symbol-mode=, we should just hook it to the =prog-mode=:
#+begin_src emacs-lisp
(use-package auto-highlight-symbol
:hook ((prog-mode . auto-highlight-symbol-mode)))
#+end_src
Similarly, the [[https://github.com/wolray/symbol-overlay][symbol-overlay]] project highlights instances of symbols, but like =iedit= creates a keymap allowing manipulation of the symbols. The workflow is:
1. ~SPC t s s~ to highlight the symbol at point.
2. ~n~ and ~p~ to move from symbol to symbol.
3. Quitting the menu involves one of these:
- ~q~ to leave point at spot
- ~e~ to return to previous cursor placement
- ~x~ to un-highlight all symbols
4. ~SPC t s s~ to highlight another symbol at point.
5. ~N~ and ~P~ to move to a /different symbol/.
6. ~r~ to rename symbol (like =iedit=)
#+begin_src emacs-lisp
(use-package symbol-overlay
:config
(pretty-hydra-define symbol-overlay (:color pink :quit-key "q")
("Show"
(("s" symbol-overlay-put "highlight")
("t" symbol-overlay-toggle-in-scope "in scope"))
"Navigate"
(("n" symbol-overlay-jump-next "next") ; j?
("p" symbol-overlay-jump-prev "previous") ; k?
("<" symbol-overlay-jump-first "first")
(">" symbol-overlay-jump-last "last"))
"Switch"
(("N" symbol-overlay-switch-forward "next")
("P" symbol-overlay-switch-backward "previous")
("d" symbol-overlay-jump-to-definition "definition"))
"Edit"
(("r" symbol-overlay-rename "replace" :color blue)
("R" symbol-overlay-query-replace "query replace" :color blue))
"Misc"
(("w" symbol-overlay-save-symbol "to clipboard") ; y?
("C-s" symbol-overlay-isearch-literally "search all" :color blue))
"Exit"
(("e" symbol-overlay-echo-mark "return" :color blue)
("x" symbol-overlay-remove-all "hide all" :color blue)
("q" nil "leave" :color blue))))
(ha-leader "t s" '("symbols" . symbol-overlay/body)))
#+end_src
While I created a Hydra for the commands,
this project includes a keymap available only when the cursor (point) is on a highlighted symbol. These keybindings include:
- ~n~ :: next matching symbol
- ~p~ :: previous matching symbol
- < :: jump first
- > :: jump last
- ~d~ :: jump to definition
- ~e~ :: return to original point position
- ~h~ :: help
- ~i~ :: unhighlight symbol
- ~q~ :: query replace
- ~r~ :: rename
- ~s~ :: isearch
- ~t~ :: toggle in scope
- ~w~ :: save symbol to clipboard
After reading [[https://lmno.lol/alvaro/its-all-up-for-grabs-and-it-compounds][this essay]] by Álvaro Ramírez, Ive been thinking of ways to connect services together. In my case, I am not sure I need [[https://github.com/magnars/multiple-cursors.el][multiple cursors]] (as symbol-overlay can rename the symbol which would be 90% of my use case), but I would like to highlight a symbol without actually moving to it.
#+begin_src emacs-lisp
(use-package symbol-overlay
:after avy
:config
(defun avy-action-highlight-symbol (pt foobar)
"Highlight symbol starting at PT at the current point."
(save-excursion
(avy-action-goto pt foobar)
(symbol-overlay-put))
t)
(add-to-list 'avy-dispatch-alist '(?S . avy-action-highlight-symbol)))
#+end_src
** Spell Checking Comments
The [[https://www.emacswiki.org/emacs/FlySpell#h5o-2][flyspell-prog-mode]] checks for misspellings in comments.
#+begin_src emacs-lisp
(use-package flyspell
:hook (prog-mode . flyspell-prog-mode))
#+end_src
** Linting with Flycheck
Why use [[https://www.flycheck.org/][flycheck]] over the built-in =flymake=? Speed used to be the advantage, but Im now pushing much of this to LSP, so speed is less of an issue. What about when I am not using LSP? Also, since Ive hooked grammar checkers, I need this with global keybindings.
#+begin_src emacs-lisp
(use-package flycheck
:straight (:host github :repo "flycheck/flycheck")
:init
(setq next-error-message-highlight t)
:bind (:map flycheck-error-list-mode-map
("C-n" . 'flycheck-error-list-next-error)
("C-p" . 'flycheck-error-list-previous-error)
("j" . 'flycheck-error-list-next-error)
("k" . 'flycheck-error-list-previous-error))
:config
(defun flycheck-enable-checker ()
"Not sure why flycheck disables working checkers."
(interactive)
(let (( current-prefix-arg '(4))) ; C-u
(call-interactively 'flycheck-disable-checker)))
(flymake-mode -1)
(global-flycheck-mode)
(ha-leader "t c" 'flycheck-mode)
(ha-leader
">" '("next problem" . flycheck-next-error)
"<" '("previous problem" . flycheck-previous-error)
"e" '(:ignore t :which-key "errors")
"e n" '(flycheck-next-error :repeat t :wk "next")
"e N" '(flycheck-next-error :repeat t :wk "next")
"e p" '(flycheck-previous-error :repeat t :wk "previous")
"e P" '(flycheck-previous-error :repeat t :wk "previous")
"e b" '("error buffer" . flycheck-buffer)
"e c" '("clear" . flycheck-clear)
"e l" '("list all" . flycheck-list-errors)
"e g" '("goto error" . counsel-flycheck)
"e y" '("copy errors" . flycheck-copy-errors-as-kill)
"e s" '("select checker" . flycheck-select-checker)
"e ?" '("describe checker" . flycheck-describe-checker)
"e h" '("display error" . flycheck-display-error-at-point)
"e e" '("explain error" . flycheck-explain-error-at-point)
"e H" '("help" . flycheck-info)
"e i" '("manual" . flycheck-manual)
"e V" '("verify-setup" . flycheck-verify-setup)
"e v" '("version" . flycheck-verify-checker)
"e E" '("enable checker" . flycheck-enable-checker)
"e x" '("disable checker" . flycheck-disable-checker)
"e t" '("toggle flycheck" . flycheck-mode)))
#+end_src
** Language Documentation
Used to use the Dash project for searching documentation associated with a programming language, but that hardly worked on my Linux systems.
Im interested in using [[https://devdocs.io/][devdocs.io]] instead, which is similar, but displays it in simple HTML. This can keep it all /inside/ Emacs. Two Emacs projects compete for this position. The Emacs [[https://github.com/astoff/devdocs.el][devdocs]] project is active, and seems to work well. Its advantage is a special mode for moving around the documentation.
#+begin_src emacs-lisp
(use-package devdocs
:general (:states 'normal
"gD" '("devdocs" . ha-devdocs-major-mode))
:config
(pretty-hydra-define hydra-devdocs (:color blue)
("Dev Docs"
(("d" ha-devdocs-major-mode "open")
("p" devdocs-peruse "peruse"))
"Packages"
(("i" devdocs-install "install")
("u" devdocs-update-all "update")
("x" devdocs-delete "uninstall")))))
#+end_src
The =devdocs-lookup= command attempts to guess which documentation it should display based on the mode, but if Im editing YAML files, I actually want to pull up the Ansible documentation, and probably the Jinja ones too.
#+begin_src emacs-lisp :tangle no
(defun ha-devdocs-major-mode ()
"My mapping of major mode to Devdocs slug."
(interactive)
(let ((devdocs-current-docs
(cl-case major-mode
('emacs-lisp-mode '("elisp"))
('python-mode '("python~.3.11"))
('yaml-ts-mode '("ansible" "jinja-2.11")))))
(devdocs-lookup nil)))
#+end_src
** Navigation
*** Move by Functions
The =mark-paragraph= and =downcase-word= isnt very useful in a programming context, and makes more sense to use them to jump around function-by-function:
#+begin_src emacs-lisp
; (global-set-key (kbd "M-k") 'beginning-of-defun)
; (global-set-key (kbd "M-j") 'beginning-of-next-defun)
(when (fboundp 'evil-define-key)
(evil-define-key '(normal insert emacs) prog-mode-map
(kbd "M-k") 'beginning-of-defun
(kbd "M-j") 'beginning-of-next-defun))
#+end_src
But one of those functions doesnt exist:
#+begin_src emacs-lisp
(defun beginning-of-next-defun (count)
"Move to the beginning of the following function."
(interactive "P")
(end-of-defun count)
(end-of-defun)
(beginning-of-defun))
#+end_src
*** Tree Sitter
Im curious about the new [[https://emacs-tree-sitter.github.io/][Tree Sitter feature]] now [[https://lists.gnu.org/archive/html/emacs-devel/2022-11/msg01443.html][built into Emacs 29]]. After following along with Mickey Petersens [[https://www.masteringemacs.org/article/how-to-get-started-tree-sitter][Getting Started with Tree Sitter]] guide, Ive concluded I /currently/ dont need this feature. Im leaving the code here, but adding a =:tangle no= to all the blocks until Im ready to re-investigate.
**** Operating System Part
Install the binary for the [[https://tree-sitter.github.io/][tree-sitter project]]. For instance:
#+begin_src sh
brew install tree-sitter npm # Since most support packages need that too.
#+end_src
The tree-sitter project does not install any language grammars by default—after all, it would have no idea which particular languages to parse and analyze!
Next, using the =tree-sitter= command line tool, create the [[/Users/howard.abrams/Library/Application Support/tree-sitter/config.json][config.json]] file:
#+begin_src sh
tree-sitter init-config
#+end_src
Normally, you would need to add all the projects to directory clones in =~/src=, e.g.
#+begin_src sh :dir ~/src
while read REPO
do
LOCATION=~/src/$(basename ${REPO})
if [ ! -d ${LOCATION} ]
then
git clone ${REPO} ${LOCATION}
fi
cd ${LOCATION}
git pull origin
npm install
done <<EOL
https://github.com/tree-sitter/tree-sitter-go
https://github.com/tree-sitter/tree-sitter-javascript
https://github.com/tree-sitter/tree-sitter-templ
https://github.com/tree-sitter-grammars/tree-sitter-yaml
https://github.com/tree-sitter/tree-sitter-json
https://github.com/tree-sitter/tree-sitter-css
https://github.com/tree-sitter/tree-sitter-python
https://github.com/tree-sitter/tree-sitter-bash
https://github.com/tree-sitter/tree-sitter-ruby
https://github.com/camdencheek/tree-sitter-dockerfile
https://github.com/alemuller/tree-sitter-make
https://github.com/Wilfred/tree-sitter-elisp
EOL
#+end_src
Seems that Docker is a bit of an odd-ball:
#+begin_src sh
mkdir -p ~/src
git -C ~/src clone https://github.com/camdencheek/tree-sitter-dockerfile
make -C ~/src/tree-sitter-dockerfile && \
make -C ~/src/tree-sitter-dockerfile install
if [[ $(uname -n) = "Darwin" ]]
then
cp ~/src/tree-sitter-dockerfile/libtree-sitter-dockerfile.dylib \
~/.emacs.d/tree-sitter
else
cp ~/src/tree-sitter-dockerfile/libtree-sitter-dockerfile.so \
~/.emacs.d/tree-sitter
fi
#+end_src
In most cases,the =npm install= /usually/ works, but I may work on some sort of various process, for instance:
#+begin_src shell
for TSS in ~/src/tree-sitter-*
do
cd $TSS
NAME=$(pwd | sed 's/.*-//')
git pull origin
npm install || make install # Various build processes!?
echo "Do we need to copy the library into ~/.emacs.d/tree-sitter/$NAME ?"
if [[ -e libtree-sitter-$NAME.dylib ]]
then
cp libtree-sitter-$NAME.dylib ~/.emacs.d/tree-sitter
fi
if [[ -e libtree-sitter-$NAME.so ]]
then
cp libtree-sitter-$NAME.so ~/.emacs.d/tree-sitter
fi
done
#+end_src
At this point, we can now parse stuff using: =tree-sitter parse <source-code-file>=
**** Emacs Part
Note that Emacs already has the ability to download and install grammars, so following instructions from Mickey Petersens essay on [[https://www.masteringemacs.org/article/combobulate-structured-movement-editing-treesitter][using Tree-sitter with Combobulate]]:
#+begin_src emacs-lisp
(when (treesit-available-p)
(use-package treesit
:straight (:type built-in)
:preface
(setq treesit-language-source-alist
'((bash "https://github.com/tree-sitter/tree-sitter-bash")
;; (c "https://github.com/tree-sitter/tree-sitter-c/" "master" "src")
(clojure "https://github.com/sogaiu/tree-sitter-clojure" "master" "src")
;; (cpp "https://github.com/tree-sitter/tree-sitter-cpp/" "master" "src")
;; (cmake "https://github.com/uyha/tree-sitter-cmake")
(css "https://github.com/tree-sitter/tree-sitter-css")
(dockerfile "https://github.com/camdencheek/tree-sitter-dockerfile" "main" "src")
;; From my private cloned repository:
;; (dockerfile "file:///opt/src/github/tree-sitter-dockerfile" "main" "src")
;; The Emacs Lisp Tree Sitter doesn't work with Emacs (go figure):
;; (elisp "https://github.com/Wilfred/tree-sitter-elisp")
;; (elixir "https://github.com/elixir-lang/tree-sitter-elixir" "main" "src")
;; (erlang "https://github.com/WhatsApp/tree-sitter-erlang" "main" "src")
(go "https://github.com/tree-sitter/tree-sitter-go")
(templ "https://github.com/vrischmann/tree-sitter-templ")
;; (haskell "https://github.com/tree-sitter/tree-sitter-haskell" "master" "src")
(html "https://github.com/tree-sitter/tree-sitter-html")
;; (java "https://github.com/tree-sitter/tree-sitter-java" "master" "src")
;; (javascript "https://github.com/tree-sitter/tree-sitter-javascript" "master" "src")
(json "https://github.com/tree-sitter/tree-sitter-json")
;; (julia "https://github.com/tree-sitter/tree-sitter-julia" "master" "src")
;; (lua "https://github.com/MunifTanjim/tree-sitter-lua" "main" "src")
(make "https://github.com/alemuller/tree-sitter-make")
(markdown "https://github.com/ikatyang/tree-sitter-markdown")
;; (meson "https://github.com/Decodetalkers/tree-sitter-meson" "master" "src")
(python "https://github.com/tree-sitter/tree-sitter-python")
(ruby "https://github.com/tree-sitter/tree-sitter-ruby" "master" "src")
(rust "https://github.com/tree-sitter/tree-sitter-rust" "master" "src")
(toml "https://github.com/tree-sitter/tree-sitter-toml")
;; (tsx "https://github.com/tree-sitter/tree-sitter-typescript" "master" "tsx/src")
;; (typescript "https://github.com/tree-sitter/tree-sitter-typescript" "master" "typescript/src")
(yaml "https://github.com/tree-sitter-grammars/tree-sitter-yaml")))
(defun mp-setup-install-grammars ()
"Install Tree-sitter grammars if they are absent."
(interactive)
(sit-for 30)
(mapc #'treesit-install-language-grammar (mapcar #'car treesit-language-source-alist)))
;; Optional, but Mickey recommends. Tree-sitter enabled major
;; modes are distinct from their ordinary counterparts, however,
;; the `tree-sitter-mode' can't be enabled if we use this
;; feature.
;;
;; You can remap major modes with `major-mode-remap-alist'. Note
;; this does *not* extend to hooks! Make sure you migrate them also
;; (dolist (mapping '((bash-mode . bash-ts-mode)
;; (sh-mode . bash-ts-mode)
;; (css-mode . css-ts-mode)
;; (dockerfile-mode . dockerfile-ts-mode)
;; (json-mode . json-ts-mode)
;; (makefile-mode . makefile-ts-mode)
;; (python-mode . python-ts-mode)
;; (ruby-mode . ruby-ts-mode)
;; (yaml-mode . yaml-ts-mode)))
;; (add-to-list 'major-mode-remap-alist mapping))
;; Can we (do we need to) update this list?
;; (add-to-list 'tree-sitter-major-mode-language-alist mapping))
:config
(mp-setup-install-grammars)))
#+end_src
And enable the languages:
#+begin_src emacs-lisp :tangle no
(when (treesit-available-p)
(use-package tree-sitter-langs
:after treesit
:config
(global-tree-sitter-mode)))
#+end_src
*** Combobulate
I like [[file:ha-programming-elisp.org::*Clever Parenthesis][Clever Parenthesis]], but can we extend that to other languages generally? After reading Mickey Petersens essay, [[https://www.masteringemacs.org/article/combobulate-structured-movement-editing-treesitter][Combobulate project]], I decided to try out his [[https://github.com/mickeynp/combobulate][combobulate package]]. Of course, this can only work with the underlying tooling supplied by the [[https://emacs-tree-sitter.github.io/][Tree Sitter]] →
#+begin_src emacs-lisp
(when (treesit-available-p)
(use-package combobulate
:straight (:host github :repo "mickeynp/combobulate")
:after treesit
:hook ((yaml-ts-mode . combobulate-mode)
;; (css-ts-mode . combobulate-mode)
;; (json-ts-mode . combobulate-mode)
;; (python-ts-mode . combobulate-mode)
)
))
#+end_src
Now, I can create an /interface/ of keystrokes to jump around like a boss:
#+begin_src emacs-lisp
(when (treesit-available-p)
(use-package combobulate
:general
(:states 'visual :keymaps 'combobulate-key-map
"o" '("mark node" . combobulate-mark-node-dwim)) ; Mark symbol since "o" doesn't do anything
(:states 'normal :keymaps 'combobulate-key-map
"g J" '("avy jump" . combobulate-avy)
"[ [" '("prev node" . combobulate-navigate-logical-previous)
"] ]" '("next node" . combobulate-navigate-logical-next)
"[ f" '("prev defun" . combobulate-navigate-beginning-of-defun)
"] f" '("next defun" . combobulate-navigate-end-of-defun)
"[ m" '("drag back" . combobulate-drag-up)
"] m" '("drag forward" . combobulate-drag-down)
"[ r" '("raise" . combobulate-vanish-node)
"g j" '(:ignore t :which-key "combobulate jump")
"g j j" '("all" . combobulate-avy-jump)
"g j s" '("strings" . ha-combobulate-string)
"g j c" '("comments" . ha-combobulate-comment)
"g j i" '("conditionals" . ha-combobulate-conditional)
"g j l" '("loops" . ha-combobulate-loop)
"g j f" '("functions" . combobulate-avy-jump-defun))
:pretty-hydra
((:color pink :quit-key "q")
("Navigation"
(("j" combobulate-navigate-logical-next "Next")
("k" combobulate-navigate-logical-previous "Previous")
("h" combobulate-navigate-beginning-of-defun "Defun <")
("l" combobulate-navigate-end-of-defun "Defun >")
("g" combobulate-avy-jump "Avy Jump"))
"Push"
(("U" combobulate-drag-up "Drag back")
("D" combobulate-drag-down "Drag forward")
("R" combobulate-vanish-node "Drag back"))
"Jump"
(("s" ha-combobulate-string "to string" :color blue)
("c" ha-combobulate-comment "comments" :color blue)
("i" ha-combobulate-conditional "conditionals" :color blue)
("l" ha-combobulate-loop "loops" :color blue)
("f" combobulate-avy-jump-defun "to defuns" :color blue))))))
#+end_src
Mickeys interface is the [[help:combobulate][combobulate]] function (or ~C-c o o~), but mine is more /evil/.
I can create a /helper function/ to allow me to jump to various types of—well, /types/:
#+begin_src emacs-lisp
(when (treesit-available-p)
(use-package combobulate
:config
(defun ha-combobulate-string ()
"Call `combobulate-avy-jump' searching for strings."
(interactive)
(with-navigation-nodes (:nodes '("string"))
(combobulate-avy-jump))))
(defun ha-combobulate-comment ()
"Call `combobulate-avy-jump' searching for comments."
(interactive)
(with-navigation-nodes (:nodes '("comment"))
(combobulate-avy-jump)))
(defun ha-combobulate-conditional ()
"Call `combobulate-avy-jump' searching for conditionals."
(interactive)
(with-navigation-nodes (:nodes '("conditional_expression"
"if_statement"
"if_clause" "else_clause"
"elif_clause"))
(combobulate-avy-jump)))
(defun ha-combobulate-loop ()
"Call `combobulate-avy-jump' searching for loops."
(interactive)
(with-navigation-nodes (:nodes '("for_statement" "for_in_clause"
"while_statement" "list_comprehension"
"dictionary_comprehension"
"set_comprehension"))
(combobulate-avy-jump))))
#+end_src
*** Evil Text Object from Tree Sitter
With Emacs version 29, we get a better approach to parsing languages, and this means that our [[https://github.com/nvim-treesitter/nvim-treesitter-textobjects#built-in-textobjects][text objects]] can be better too with the [[https://github.com/meain/evil-textobj-tree-sitter][evil-textobj-tree-sitter project]]:
#+begin_src emacs-lisp :tangle no
(when (and (treesit-available-p) (fboundp 'evil-define-text-object))
(use-package evil-textobj-tree-sitter
:config
;; We need to bind keys to the text objects found at:
;; https://github.com/nvim-treesitter/nvim-treesitter-textobjects#built-in-textobjects
;; bind `function.outer`(entire function block) to `f` for use in things like `vaf`, `yaf`
(define-key evil-outer-text-objects-map "f" (evil-textobj-tree-sitter-get-textobj "function.outer"))
;; bind `function.inner`(function block without name and args) to `f` for use in things like `vif`, `yif`
(define-key evil-inner-text-objects-map "f" (evil-textobj-tree-sitter-get-textobj "function.inner"))
(define-key evil-outer-text-objects-map "c" (evil-textobj-tree-sitter-get-textobj "comment.outer"))
(define-key evil-inner-text-objects-map "c" (evil-textobj-tree-sitter-get-textobj "comment.inner"))
(define-key evil-outer-text-objects-map "u" (evil-textobj-tree-sitter-get-textobj "conditional.outer"))
(define-key evil-inner-text-objects-map "u" (evil-textobj-tree-sitter-get-textobj "conditional.inner"))
(define-key evil-outer-text-objects-map "b" (evil-textobj-tree-sitter-get-textobj "loop.outer"))
(define-key evil-inner-text-objects-map "b" (evil-textobj-tree-sitter-get-textobj "loop.inner"))))
#+end_src
Seems the macro, =evil-textobj-tree-sitter-get-textobj= has a bug, so the following—which would have been easier to write—doesnt work:
#+begin_src emacs-lisp :tangle no :tangle no
(dolist (combo '(("f" "function.outer" "function.inner")
("b" "loop.outer" "loop.inner")
;; ...
("c" "comment.outer" "comment.inner")))
(destructuring-bind (key outer inner) combo
;; bind an outer (e.g. entire function block) for use in things like `vaf`, `yaf` combo
(define-key evil-outer-text-objects-map key (evil-textobj-tree-sitter-get-textobj outer))
;; bind an inner (e.g. function block without name and args) for use in things like `vif`, `yif`
(define-key evil-inner-text-objects-map key (evil-textobj-tree-sitter-get-textobj inner))))
#+end_src
*** dumb-jump
Once upon a time, we use to create a =TAGS= file that contained the database for navigating code bases, but with new faster versions of grep, e.g. [[https://beyondgrep.com][ack]], [[https://github.com/ggreer/the_silver_searcher][ag]] (aka, the Silver Searcher), [[https://github.com/Genivia/ugrep][ugrep]] and [[https://github.com/BurntSushi/ripgrep][ripgrep]], we should be able to use them. but I want to:
- Be in a function, and see its callers. For this, the [[help:rg-dwim][rg-dwim]] function is my bread-and-butter.
- Be on a function, and jump to the definition. For this, I use [[https://github.com/jacktasia/dumb-jump][dumb-jump]], which uses the above utilities.
#+begin_src emacs-lisp
(use-package dumb-jump
:config
(setq dumb-jump-prefer-searcher 'rg
xref-history-storage #'xref-window-local-history
xref-show-definitions-function #'xref-show-definitions-completing-read)
(add-hook 'xref-backend-functions #'dumb-jump-xref-activate)
;; Never using the etags backend. GNU Global? Maybe.
(remove-hook 'xref-backend-functions #'etags--xref-backend))
#+end_src
While Im at it, lets connect various ~g~ sequence keys to =xref-= interface functions:
#+begin_src emacs-lisp
(use-package emacs
:general
(:states 'normal
"g ." '("find def" . xref-find-definitions)
"g >" '("find def o/win" . xref-find-definitions-other-window)
"g ," '("def go back" . xref-go-back)
"g <" '("def go forward" . xref-go-forward)
"g /" '("find refs" . xref-find-references)
"g ?" '("find/rep refs" . xref-find-references-and-replace)
"g h" '("find apropos" . xref-find-apropos)
"g b" '("def go back" . xref-go-back)))
#+end_src
I have two different /jumping/ systems, the [[info:emacs#Xref][Xref interface]] and Evils. While comparable goals, they are behave different. Lets compare evil keybindings:
| ~M-.~ | ~g .~ | [[help:xref-find-definitions][xref-find-definitions]] (also ~g d~ for [[help:evil-goto-definition][evil-goto-definition]])† |
| | ~g >~ | =xref-find-definitions-other-window= |
| ~M-,~ | ~g ,~ | [[help:xref-go-back][xref-go-back]] (see [[help:xref-pop-marker-stack][xref-pop-marker-stack]]) |
| ~C-M-,~ | ~g <~ | [[help:xref-go-forward][xref-go-forward]] (kinda like =xref-find-definitions=) |
| ~M-?~ | ~g /~ | [[help:xref-find-references][xref-find-references]] to go from definition to code calls‡ |
| | ~g ?~ | [[help:xref-find-references-and-replace][xref-find-references-and-replace]] could be more accurate than [[*iEdit][iEdit]]. |
| ~C-M-.~ | ~g h~ | [[help:xref-find-apropos][xref-find-apropos]] … doesnt work well without LSP |
| ~C-TAB~ | | perform completion around point (also ~M-TAB~), see [[file:ha-config.org::*Auto Completion][Auto Completion]]. |
† Prefix to prompt for the term \
‡ If it finds more than one definition, Emacs displays the [[info:emacs#Xref Commands][*xref* buffer]], allowing you to select the definition.
** Language Server Protocol (LSP) Integration
The [[https://microsoft.github.io/language-server-protocol/][LSP]] is a way to connect /editors/ (like Emacs) to /languages/ (like Lisp)… wait, no. While originally designed for VS Code and probably Python, we can abstract away [[https://github.com/davidhalter/jedi][Jedi]] and the [[http://tkf.github.io/emacs-jedi/latest/][Emacs integration to Jedi]] (and duplicate everything for Ruby, and Clojure, and…).
Emacs has two LSP projects, and while I have used [[LSP Mode]], but since I dont have heavy IDE requirements, I am finding that [[eglot]] to be simpler.
#+begin_src emacs-lisp
(use-package eglot
:straight nil
:commands eglot-ensure
:config
(setq eglot-autoshutdown t))
#+end_src
The [[https://github.com/flycheck/flycheck-eglot][flycheck-eglot]] package bridges eglot diagnostics into flycheck, so existing checkers and keybindings continue to work unchanged:
#+begin_src emacs-lisp
(use-package flycheck-eglot
:after (flycheck eglot)
:config
(global-flycheck-eglot-mode 1))
#+end_src
** General Code Editing
*** iEdit
While there are language-specific ways to rename variables and functions, [[https://github.com/victorhge/iedit][iedit]] is often sufficient.
#+begin_src emacs-lisp :tangle no
(use-package iedit
:config
(ha-leader "s e" '("iedit" . iedit-mode)))
#+end_src
While =iedit= acts a little odd with Evil, the [[https://github.com/syl20bnr/evil-iedit-state][evil-iedit-state project]] attempts to makes the interface more intuitive.
This creates both an =iedit= and =iedit-insert= states. Calling ~Escape~ from =iedit-insert= goes to =iedit=, and hitting it again, will go back to =normal= state.
To use, highlight a region with ~v~, and continue to hit ~v~ until youve selected the variable/symbol, and then type ~e~. Or, highlight normally, e.g. ~v i o~, and hit ~E~:
#+begin_src emacs-lisp
(when (fboundp 'evil-mode)
(use-package evil-iedit-state
:after iedit
:general
(:states 'visual "E" '("iedit" . evil-iedit-state/iedit-mode))))
#+end_src
The =iedit-insert= state is pretty much /regular/ =insert= state, so the interesting keys are in =iedit= state:
- ~0~ / ~$~ :: jump to beginning/end of the “occurrence”
- ~n~ / ~N~ :: jump to next / previous occurrence
- ~I~ / ~A~ :: jump to beginning/end of occurrence and go into =iedit-insert= mode (obviously ~a~ and ~i~ do too)
- ~#~ :: highlights all the matching occurrences
- ~F~ :: restricts to the current function
*** Case Conversion
The [[https://github.com/akicho8/string-inflection][string-inflection]] project (see [[http://sodaware.sdf.org/notes/converting-to-snake-case-in-emacs/][this overview]]) converts symbol variables to /appropriate format/ for the mode. This replaces my home-brewed functions.
#+begin_src emacs-lisp
(use-package string-inflection
:general
(:states '(normal visual motion operator)
"z s" '("to snake case" . string-inflection-underscore)
"z S" '("to Snake Case" . string-inflection-upcase)
"z c" '("to camelCase" . string-inflection-lower-camelcase)
"z C" '("to CamelCase" . string-inflection-camelcase)
"z -" '("to kebab case" . string-inflection-kebab-case)
"z z" '("toggle snake/camel" . string-inflection-all-cycle)))
#+end_src
I would like to have this bound on the ~g~ sequence, but that is crowded.
Note that ~g u~ (for lower-casing stuff), and ~g U~ (for up-casing) requires /something/, for instance ~g U i o~ upper-cases the symbol at point. These functions, however, only work with a symbol (which is the typical case).
** Inline Code Evaluation
While I like [[help:eval-print-last-sexp][eval-print-last-sexp]], I would like a bit of formatting in order to /keep the results/ in the file.
#+begin_src emacs-lisp
(defun ha-eval-print-last-sexp (&optional internal-arg)
"Evaluate the expression located before the point.
Insert results back into the buffer at the end of the line after
a comment."
(interactive)
(save-excursion
(eval-print-last-sexp internal-arg))
(end-of-line)
(insert " ")
(insert comment-start)
(insert "")
(dotimes (i 2)
(next-line)
(join-line)))
#+end_src
Typical keybindings for all programming modes:
#+begin_src emacs-lisp
(ha-local-leader :keymaps 'prog-mode-map
"e" '(:ignore t :which-key "eval")
"e ;" '("expression" . eval-expression)
"e b" '("buffer" . eval-buffer)
"e f" '("function" . eval-defun)
"e r" '("region" . eval-region)
"e e" '("eval exp" . eval-last-sexp)
"e p" '("print s-exp" . ha-eval-print-last-sexp))
#+end_src
** Ligatures
The idea of using math symbols for a programming languages keywords is /cute/, but can be confusing, so I use it sparingly:
#+begin_src emacs-lisp
(defun ha-prettify-prog ()
"Extends the `prettify-symbols-alist' for programming."
(mapc (lambda (pair) (push pair prettify-symbols-alist))
'(("lambda" . "𝝀")
(">=" . "")
("<=" . "")
("!=" . "")))
(prettify-symbols-mode))
(add-hook 'prog-mode-hook 'ha-prettify-prog)
#+end_src
Hopefully I can follow [[https://www.masteringemacs.org/article/unicode-ligatures-color-emoji][Mickey Petersen's essay]] on getting full ligatures working, but right now, they dont work on the Mac, and that is my current workhorse.
#+begin_src emacs-lisp
(use-package ligature
:config
;; Enable the "www" ligature in every possible major mode
(ligature-set-ligatures 't '("www"))
;; Enable traditional ligature support in eww-mode, if the
;; `variable-pitch' face supports it
(ligature-set-ligatures '(org-mode eww-mode) '("ff" "fi" "ffi"))
(ligature-set-ligatures '(html-mode nxml-mode web-mode)
'("<!--" "-->" "</>" "</" "/>" "://"))
;; Create a new ligature:
(ligature-set-ligatures 'markdown-mode '(("=" (rx (+ "=") (? (| ">" "<"))))
("-" (rx (+ "-")))))
;; Enable all Cascadia Code ligatures in programming modes
(ligature-set-ligatures
'prog-mode '("|||>" "<|||" "<==>" "<!--" "####" "~~>" "***" "||=" "||>"
":::" "::=" "=:=" "===" "==>" "=!=" "=>>" "=<<" "=/=" "!=="
"!!." ">=>" ">>=" ">>>" ">>-" ">->" "->>" "-->" "---" "-<<"
"<~~" "<~>" "<*>" "<||" "<|>" "<$>" "<==" "<=>" "<=<" "<->"
"<--" "<-<" "<<=" "<<-" "<<<" "<+>" "</>" "###" "#_(" "..<"
"..." "+++" "/==" "///" "_|_" "www" "&&" "^=" "~~" "~@" "~="
"~>" "~-" "**" "*>" "*/" "||" "|}" "|]" "|=" "|>" "|-" "{|"
"[|" "]#" "::" ":=" ":>" ":<" "$>" "==" "=>" "!=" "!!" ">:"
">=" ">>" ">-" "-~" "-|" "->" "--" "-<" "<~" "<*" "<|" "<:"
"<$" "<=" "<>" "<-" "<<" "<+" "</" "#{" "#[" "#:" "#=" "#!"
"##" "#(" "#?" "#_" "%%" ".=" ".-" ".." ".?" "+>" "++" "?:"
"?=" "?." "??" ";;" "/*" "/=" "/>" "//" "__" "~~" "(*" "*)"
"\\\\" "://"))
;; Enables ligature checks globally in all buffers. You can also do it
;; per mode with `ligature-mode'.
(global-ligature-mode t))
#+end_src
Until I can get [[https://github.com/d12frosted/homebrew-emacs-plus/issues/222][Harfbuzz support]] on my Emacs-Plus build of Mac, the following work-around seems to mostly work:
#+begin_src emacs-lisp
(defun ha-mac-litagure-workaround ()
"Implement an old work-around for ligature support.
This kludge seems to only need to be set for my Mac version of
Emacs, since I can't build it with Harfuzz support."
(let ((alist '((33 . ".\\(?:\\(?:==\\|!!\\)\\|[!=]\\)")
(35 . ".\\(?:###\\|##\\|_(\\|[#(?[_{]\\)")
(36 . ".\\(?:>\\)")
(37 . ".\\(?:\\(?:%%\\)\\|%\\)")
(38 . ".\\(?:\\(?:&&\\)\\|&\\)")
(42 . ".\\(?:\\(?:\\*\\*/\\)\\|\\(?:\\*[*/]\\)\\|[*/>]\\)")
(43 . ".\\(?:\\(?:\\+\\+\\)\\|[+>]\\)")
(45 . ".\\(?:\\(?:-[>-]\\|<<\\|>>\\)\\|[<>}~-]\\)")
(46 . ".\\(?:\\(?:\\.[.<]\\)\\|[.=-]\\)")
(47 . ".\\(?:\\(?:\\*\\*\\|//\\|==\\)\\|[*/=>]\\)")
(48 . ".\\(?:x[a-zA-Z]\\)")
(58 . ".\\(?:::\\|[:=]\\)")
(59 . ".\\(?:;;\\|;\\)")
(60 . ".\\(?:\\(?:!--\\)\\|\\(?:~~\\|->\\|\\$>\\|\\*>\\|\\+>\\|--\\|<[<=-]\\|=[<=>]\\||>\\)\\|[*$+~/<=>|-]\\)")
(61 . ".\\(?:\\(?:/=\\|:=\\|<<\\|=[=>]\\|>>\\)\\|[<=>~]\\)")
(62 . ".\\(?:\\(?:=>\\|>[=>-]\\)\\|[=>-]\\)")
(63 . ".\\(?:\\(\\?\\?\\)\\|[:=?]\\)")
(91 . ".\\(?:]\\)")
(92 . ".\\(?:\\(?:\\\\\\\\\\)\\|\\\\\\)")
(94 . ".\\(?:=\\)")
(119 . ".\\(?:ww\\)")
(123 . ".\\(?:-\\)")
(124 . ".\\(?:\\(?:|[=|]\\)\\|[=>|]\\)")
(126 . ".\\(?:~>\\|~~\\|[>=@~-]\\)"))))
(dolist (char-regexp alist)
(set-char-table-range composition-function-table (car char-regexp)
`([,(cdr char-regexp) 0 font-shape-gstring])))))
(unless (s-contains? "HARFBUZZ" system-configuration-features)
(add-hook 'prog-mode-hook #'ha-mac-litagure-workaround))
#+end_src
The unicode-fonts package rejigs the internal tables Emacs uses to pick better fonts for unicode codepoint ranges.
#+begin_src emacs-lisp :tangle no
(use-package unicode-fonts
:config
(ignore-errors
(unicode-fonts-setup)))
#+end_src
** Compiling
The [[help:compile][compile]] function lets me enter a command to run, or I can search the history for a previous run. What it doesnt give me, is a project-specific list of commands. Perhaps, for each project, I define in =.dir-locals.el= a variable, =compile-command-list=, like:
#+begin_src emacs-lisp :tangle no
((nil . ((compile-command . "make -k ")
(compile-command-list . ("ansible-playbook playbooks/confluence_test.yml"
"ansible-playbook playbooks/refresh_inventory.yml")))))
#+end_src
Many of the build tools now attempt to use ANSI colorization in order to make the output pretty.
#+BEGIN_SRC emacs-lisp
(require 'ansi-color)
(defun colorize-compilation-buffer ()
(let ((inhibit-read-only t))
(ansi-color-apply-on-region (point-min) (point-max))))
(add-hook 'compilation-filter-hook 'colorize-compilation-buffer)
#+END_SRC
And lets see the results by scrolling the buffer.:
#+BEGIN_SRC emacs-lisp
(setq compilation-scroll-output t)
#+END_SRC
To make the =compile-command-list= variable less risky, we need to declare it:
#+begin_src emacs-lisp
(defvar compile-command-list nil "A list of potential commands to give to `ha-project-compile'.")
(defun ha-make-compile-command-list-safe ()
"Add the current value of `compile-command-list' safe."
(interactive)
(add-to-list 'safe-local-variable-values `(compile-command-list . ,compile-command-list)))
#+end_src
What compile commands should I have on offer? Along with the values in =compile-command-list= (if set), I could look at files in the projects root and get targets from a =Makefile=, etc. Well use helper functions I define later:
#+begin_src emacs-lisp
(defun ha--compile-command-list ()
"Return list of potential commands for a project."
(let ((default-directory (project-root (project-current))))
;; Make a list of ALL the things.
;; Note that `concat' returns an empty string if you give it null,
;; so we use `-concat' the dash library:
(-concat
compile-history
(ha--makefile-completions)
(ha--toxfile-completions)
(when (and (boundp 'compile-command-list) (listp compile-command-list))
compile-command-list))))
#+end_src
My replacement to [[help:compile][compile]] uses my new =completing-read= function:
#+begin_src emacs-lisp
(defun ha-project-compile (command)
"Run `compile' from a list of directory-specific commands."
(interactive (list (completing-read "Compile command: "
(ha--compile-command-list)
nil nil "" 'compile-history)))
(let ((default-directory (project-root (project-current))))
(cond
((string-match rx-compile-to-vterm command) (ha-compile-vterm command))
((string-match rx-compile-to-eshell command) (ha-compile-eshell command))
(t (compile command)))))
#+end_src
If I end a command with a =|v=, it sends the compile command to a vterm session for the project, allowing me to continue the commands:
#+begin_src emacs-lisp
(defvar rx-compile-to-vterm (rx "|" (0+ space) "v" (0+ space) line-end))
(defun ha-compile-vterm (full-command &optional project-dir)
(unless project-dir
(setq project-dir (project-name (project-current))))
;; (add-to-list 'compile-history full-command)
(let ((command (replace-regexp-in-string rx-compile-to-vterm "" full-command)))
(ha-ssh-send command project-dir)))
#+end_src
And what about sending the command to Eshell as well?
#+begin_src emacs-lisp
(defvar rx-compile-to-eshell (rx "|" (0+ space) "s" (0+ space) line-end))
(defun ha-compile-eshell (full-command &optional project-dir)
"Send a command to the currently running Eshell terminal.
If a terminal isn't running, it will be started, allowing follow-up
commands."
(unless project-dir
(setq project-dir (project-name (project-current))))
(let ((command (replace-regexp-in-string rx-compile-to-eshell "" full-command)))
(ha-eshell-send command project-dir)))
#+end_src
And lets add it to the Project leader:
#+begin_src emacs-lisp
(ha-leader "p C" 'ha-project-compile)
#+end_src
Note that =p c= (to call [[help:recompile][recompile]]) should still work.
Other peoples projects:
- [[https://github.com/Olivia5k/makefile-executor.el][makefile-executor.el]] :: works only with Makefiles
- [[https://github.com/tarsius/imake][imake]] :: works only with Makefiles that are formatted with a =help:= target
- [[https://github.com/emacs-taskrunner/emacs-taskrunner][Taskrunner project]] :: requires ivy or helm, but perhaps I could use the underlying infrastructure to good ol [[help:completing-read][completing-read]]
Note: Someday I may want to convert my =Makefile= projects to [[https://taskfile.dev/][Taskfile]].
*** Makefile Completion
This magic script is what Bash uses for completion when you type =make= and hit the TAB:
#+name: make-targets
#+begin_src shell :tangle no
make -qRrp : 2> /dev/null | awk -F':' '/^[a-zA-Z0-9][^$#\\/\\t=]*:([^=]|$)/ {split($1,A,/ /);for(i in A)print A[i]}'
#+end_src
Which makes it easy to get a list of completions for my compile function:
#+begin_src emacs-lisp :noweb yes
(defun ha--makefile-completions ()
"Returns a list of targets from the Makefile in the current directory."
(when (file-exists-p "Makefile")
(--map (format "make -k %s" it)
(shell-command-to-list "<<make-targets>>"))))
#+end_src
*** Python Tox Completion
Lets just grab the environments to run:
#+begin_src emacs-lisp
(defun ha--toxfile-completions ()
"Returns a list of targets from the tox.ini in the current directory."
(when (file-exists-p "tox.ini")
(--map (format "tox -e %s" it)
(shell-command-to-list "tox -a"))))
#+end_src
* Languages
Simple to configure languages go here. More advanced languages go into their own files… eventually.
** Configuration Files
So many configuration files to track:
#+begin_src emacs-lisp
(use-package conf-mode
:mode (("\\.conf\\'" . conf-space-mode)
("\\.repo\\'" . conf-unix-mode)
("\\.setup.*\\'" . conf-space-mode)))
#+end_src
** JSON
Im interested in the [[https://github.com/emacs-tree-sitter/tree-sitter-langs][tree-sitter]] extensions for JSON, e.g. =json-ts-mode=, that comes with Emacs 29, but instead of /searching/ for particular data in a JSON buffer, what about whittling that data with [[https://jqlang.github.io/jq/][jq]]?
#+begin_src emacs-lisp
(defun ha-json-buffer-to-jq (query)
"Runs JSON buffer with QUERY through an external `jq' program.
Attempts to find the first JSON object in the buffer, and limits
the data to that region. The `jq' program is the first found in
the standard path."
(interactive "sjq Query: ")
(let (s e)
(save-excursion
(if (region-active-p)
(setq s (region-beginning)
e (region-end))
(goto-char (point-min))
(unless (looking-at "{")
(re-search-forward "{")
(goto-char (match-beginning 0)))
(setq s (point))
;; Jump forward using the evil-jump-item ... change this to one
;; of the functions in thing-at-point?
(when (fboundp 'evil-jump-item)
(evil-jump-item))
(setq e (1+ (point))))
;; (narrow-to-region s e)
(shell-command-on-region s e (concat "jq " query) nil t "*jq errors*"))))
(ha-local-leader :keymaps '(js-json-mode-map json-ts-mode-map)
"j" 'ha-json-buffer-to-jq)
#+end_src
This means, that some data like:
#+begin_src json :tangle no
{
"common_id": "GMC|F2BADC23|64D52BF7|awardlateengine",
"data": {
"name": "Create And Wait for Service Image",
"description": "Creates a new Service Image using IMaaS",
"long_description": "This job creates a new service image with name ... blah, blah, blah",
"job_id": "5e077245-0f4a-4dc9-b473-ce3ec0b811ba",
"state": "success",
"progress": "100",
"timeout": {
"seconds": 300,
"strategy": "real_time",
"elapsed": 1291.8504
},
"started_at": "2023-08-10T16:20:49Z",
"finished_at": "2023-08-10T16:42:20Z",
"links": [
{
"rel": "child-4aa5978c-4537-4aa9-9568-041ad97c2374",
"href": "https://eng501.garmet.howardism.org/api/jobs/4aa5978c-4537-4aa9-9568-041ad97c2374"
},
{
"rel": "project",
"href": "https://eng501.garmet.howardism.org/api/projects/8abe0f6e-161e-4423-ab27-d4fb0d5cfd0c"
},
{
"rel": "details",
"href": "https://eng501.garmet.howardism.org/api/jobs/5e077245-0f4a-4dc9-b473-ce3ec0b811ba/details"
}
],
"tags": [
"foobar", "birdie"
],
"progress_comment": null,
"children": [
{
"id": "4aa5978c-4537-4aa9-9568-041ad97c2374"
}
]
},
"status": "SUCCESS"
}
#+end_src
I can type, ~, j~ and then type =.data.timeout.seconds= and end up with:
#+begin_src json
300
#+end_src
** Markdown
Most projects =README= files and other documentation use [[https://jblevins.org/projects/markdown-mode/][markdown-mode]]. Note that the /preview/ uses =multimarkdown=, which should be /pre-installed/. For instance:
#+begin_src sh
brew install multimarkdown
#+end_src
Also, I like Markdown to look like a word processor, similarly to my org files:
#+begin_src emacs-lisp
(use-package markdown-mode
:mode ((rx ".md" string-end) . gfm-mode)
:init (setq markdown-command (expand-file-name "markdown" "~/bin")
markdown-open-command (expand-file-name "markdown-open" "~/bin")
markdown-header-scaling t)
:general
(:states 'normal :no-autoload t :keymaps 'markdown-mode-map
", l" '("insert link" . markdown-insert-link) ; Also C-c C-l
", i" '("insert image" . markdown-insert-image) ; Also C-c C-i
;; SPC u 3 , h for a third-level header:
", h" '("insert header" . markdown-insert-header-dwim)
", t" '(:ignore t :which-key "toggles")
", t t" '("toggle markup" . markdown-toggle-markup-hiding)
", t u" '("toggle urls" . markdown-toggle-markup-url-hiding)
", t i" '("toggle images" . markdown-toggle-markup-inline-images)
", t m" '("toggle math" . markdown-toggle-markup-math-hiding)
", d" '("do" . markdown-do)
", e" '("export" . markdown-export)
", p" '("preview" . markdown-preview)))
#+end_src
Note that the markdown-specific commands use the ~C-c C-c~ and ~C-c C-s~ prefixes.
*** Rendering with Pandoc
We call both the =markdown-command= and the =markdown-open-command= variables to render (and preview) a Markdown file (~C-c C-c o~), and calls the following scripts (which in turn, call =pandoc= as I depend on this for other org-related features):
#+begin_src sh :tangle ~/bin/markdown :shebang "#!/usr/bin/env bash" :tangle-mode u+x
pandoc --to=html --from=gfm $*
#+end_src
#+begin_src sh :tangle ~/bin/markdown-open :shebang "#!/usr/bin/env bash" :tangle-mode u+x
OUTPUT_FILE=$(mktemp 'emacs-view-XXXXXXX.html')
pandoc --to=html --from=gfm --output=$OUTPUT_FILE $*
# Are we on a MacOS Laptop:
if [ -d "/Library" ]
then
open $OUTPUT_FILE
else
firefox -new-tab $OUTPUT_FILE
fi
#+end_src
*** PyMarkdown
My teams have standardized on “linting” markdown files using [[https://pypi.org/project/pymarkdownlnt/][PyMarkdown]], specifying a =.pymarkdown.yml= to configure what is allowed. For instance:
#+BEGIN_SRC yaml :tangle no
---
# Enable front-matter extensions, as Hugo needs them.
extensions:
front-matter:
enabled: true
plugins:
md013:
enabled: true
line_length: 80
heading_line_length: 80
code_block_line_length: 160
#+END_SRC
Flycheck ships the =markdown-pymarkdown= checker itself, so all it wants is the configuration file.
Note that PyMarkdown does /not/ look for =.pymarkdown.yml= on its own — it only reads a configuration passed with =--config=. Naming the file without a directory hands the search to flycheck, whose =flycheck-locate-config-file-functions= walks the ancestor directories with =locate-dominating-file= and then falls back to my home directory. A repository that defines its own rules therefore gets them, and everything else gets my defaults:
#+begin_src emacs-lisp
(use-package flycheck
:config
(setq flycheck-markdown-pymarkdown-config ".pymarkdown.yml"))
#+end_src
*** Remark
The [[https://github.com/remarkjs/remark][Remark]] project can transform markdown with plugins, evaluating and changing through its pipeline. This means I can apply [[file:ha-org.org::*Writegood][Writegood]] intelligently to a Markdown file, ignoring code blocks, etc. Prose is the /only/ job it has here, and [[*Remark and Flycheck][flycheck chains the two together]].
The =remark= executable itself lives globally, since flycheck calls it by name:
#+begin_src sh
npm install -g remark-cli
#+end_src
The /plugins/, however, can not. The configuration file below uses ESM =import= statements, and Node resolves those specifiers relative to /importing the file/, as globally installed packages are not on the path. The =~/.config/remark= directory has to be a self-contained package with its own =node_modules=, with a =package.json= file.
Note the ="type": "module"= entry belongs here too, otherwise Node parses each =.js= file as CommonJS, fails, and re-parses it as a module while complaining:
#+begin_src js :tangle ~/.config/remark/package.json :mkdirp yes
{
"name": "remark-config",
"private": true,
"type": "module",
"dependencies": {
"remark-frontmatter": "^5.0.0",
"remark-lint-write-good": "^1.2.0",
"unist-util-visit": "^5.1.0"
}
}
#+end_src
After tangling that the above file, we install the plugins /into the =.config/remark= directory/:
#+begin_src sh :dir ~/.config/remark
npm install
#+end_src
Lets create a /code-stripping/ plugin that removes all text but the prose of a Markdown document. We can then validate the prose with =write-good= on the results:
#+begin_src js :tangle ~/.config/remark/ignore-code.js :mkdirp yes
import { visit } from 'unist-util-visit';
// Bare URLs and autolinks land in `text` nodes, where write-good
// grades the path segments as if they were prose. A `[label](url)`
// needs no help here, since the url of a link node never reaches
// the prose checker anyway.
const URL_PATTERN = /\b(?:https?:\/\/|www\.)[^\s<>()[\]{}'"]+/gi;
/** Spaces of equal length, so line numbers and offsets survive. */
const blank = (text) => ' '.repeat(text.length);
export default function remarkIgnoreCode() {
return (tree) => {
visit(tree, ['code', 'inlineCode'], (node) => {
// Replaces code block and backtick content with spaces of
// equal length. This silences linters while preserving line
// numbers and offsets.
node.value = blank(node.value);
});
visit(tree, 'text', (node) => {
node.value = node.value.replace(URL_PATTERN, (match) => {
// Trailing punctuation stays, so sentence boundaries are
// not merged.
const url = match.replace(/[.,;:!?]+$/, '');
return blank(url) + match.slice(url.length);
});
});
};
}
#+end_src
A path like =/a/very/long/= reads as weasel words to =write-good=, so blanking the URL is what keeps =very= and =obviously= out of the report. The =[label](url)= form is left alone deliberately: its label /is/ prose worth grading, and its url was never visited. Trailing punctuation survives so that =…/foo.= still ends a sentence, rather than running it into the next one.
**** Configuring the pipeline
The other plugins have been made, so Remarks configuration file (=.config/remark/remarkrc.js=) pulls them in, and creates an ordered plugin pipeline. Note the absence of any =remark-preset-lint-*= entry: those presets check structure, which is PyMarkdowns half of the work, and they disagree with it often enough to be worth leaving out. Left in, they wanted every ATX heading rewritten as setext:
#+begin_src js :tangle ~/.config/remark/.remarkrc.js
import remarkFrontmatter from 'remark-frontmatter';
import remarkWriteGood from 'remark-lint-write-good';
import remarkIgnoreCode from './ignore-code.js';
export default {
plugins: [
// 1. Recognise YAML front matter, so its metadata is not
// read as prose
remarkFrontmatter,
// 2. Erase backticks/code blocks before the prose check
remarkIgnoreCode,
// 3. Check English writing quality, the sole
// job remark has, since PyMarkdown already covers
// structure and formatting.
remarkWriteGood
]
};
#+end_src
The order matters, since plugins run as transformers in sequence: =remarkIgnoreCode= has to blank the code out before =remarkWriteGood= gets a look at what is left.
And =remarkFrontmatter= earns its place, rather than being tidiness. Without it, a =---= delimiter usually turns the metadata into a setext heading, which =write-good= happens to skip — but only by luck. Put a blank line in the front matter and the first key becomes an ordinary paragraph, at which point =title:= gets graded as prose:
#+begin_src markdown :tangle no
---
title: This value is very obviously scanned as prose
description: short
---
#+end_src
Worth knowing what =write-good= looks at, since it only visits =paragraph= nodes: headings are never checked, and blockquotes are skipped on purpose, on the reasoning that quoted words are somebody elses.
Verify on the command line using the =rc-path= parameter:
#+begin_src sh :tangle no
remark --rc-path ~/.config/remark/.remarkrc.js README.md
#+end_src
**** Remark and Flycheck
Remarks default report groups messages under a file name header and gives each one a =line:column-line:column= range, which takes more regular expression than I care to write. Instead, lets hand it a [[https://github.com/vfile/vfile-reporter][vfile reporter]] of our own that prints one message per line in a shape flycheck parses with a single pattern:
#+begin_src js :tangle ~/.config/remark/flycheck-reporter.js
/**
* A vfile reporter that emits one message per line in a format that is
* trivial for Emacs' flycheck to parse with a single regular expression:
*
* LINE:COLUMN:SEVERITY:[ruleId] reason
*
* The file name is deliberately omitted; flycheck lints one buffer at a
* time and attributes patterns without a file name to that buffer, which
* avoids the mismatch between the buffer's directory and remark's cwd.
*/
export default function flycheckReporter(files) {
const lines = [];
for (const file of [files].flat()) {
for (const message of file.messages) {
// `fatal` is true for errors, false for warnings, null/undefined for info.
const severity =
message.fatal === true
? 'error'
: message.fatal === false
? 'warning'
: 'info';
const rule = message.ruleId || message.source || 'remark';
// Newlines would break the one-message-per-line contract.
const reason = String(message.reason).replace(/\s*\n\s*/g, ' ');
lines.push(
`${message.line || 1}:${message.column || 1}:${severity}:[${rule}] ${reason}`
);
}
}
return lines.join('\n');
}
#+end_src
Which we can check by hand:
#+begin_src sh
remark --no-color --no-stdout \
--rc-path ~/.config/remark/.remarkrc.js \
--report ~/.config/remark/flycheck-reporter.js README.md
#+end_src
Two flags there earn their keep. Remark writes the /transformed/ document to standard output by default, and since =remarkIgnoreCode= replaces code with whitespace, that output is a mangled copy of the file. The =--no-stdout= parameter throws it away, guaranteeing a stray =--output= can never overwrite the real file with it. Also =--no-color= keeps ANSI escapes out of what flycheck has to parse.
Note that both paths are absolute. Remark resolves a /reporter/ relative to the current directory, but a configuration file relative to the file being linted, so neither would be found from an arbitrary project directory.
Note that =flycheck-define-checker= only /defines/ the checker; the =add-to-list= is what puts it in rotation. The trailing =t= appends rather than prepends, which is the whole trick to chaining: flycheck picks the /first/ checker in =flycheck-checkers= that claims the buffer, so leaving =markdown-remark= behind the built-in =markdown-pymarkdown= lets PyMarkdown go first and remark follow it.
#+begin_src emacs-lisp
(use-package flycheck
:config
(flycheck-def-config-file-var flycheck-markdown-remark-config markdown-remark
(expand-file-name ".remarkrc.js" "~/.config/remark")
:package-version '(flycheck . "34"))
(defcustom flycheck-markdown-remark-reporter
(expand-file-name "flycheck-reporter.js" "~/.config/remark")
"Path to the vfile reporter that formats remark messages for flycheck."
:type 'string
:group 'flycheck)
(flycheck-define-checker markdown-remark
"A Markdown prose checker using remark and write-good.
See URL `https://github.com/remarkjs/remark'."
:command ("remark"
"--no-color"
"--no-stdout"
(config-file "--rc-path" flycheck-markdown-remark-config)
"--report" (eval flycheck-markdown-remark-reporter)
source)
:error-patterns
((error line-start line ":" column ":error:["
(id (one-or-more (not (any "]")))) "] " (message) line-end)
(warning line-start line ":" column ":warning:["
(id (one-or-more (not (any "]")))) "] " (message) line-end)
(info line-start line ":" column ":info:["
(id (one-or-more (not (any "]")))) "] " (message) line-end))
:modes (markdown-mode gfm-mode))
(add-to-list 'flycheck-checkers 'markdown-remark t)
;; Structure first, then prose, in one report.
(flycheck-add-next-checker 'markdown-pymarkdown 'markdown-remark))
#+end_src
Naming =markdown-remark= as a plain symbol, rather than a =(level . checker)= cons cell, means the prose check runs whatever PyMarkdown found. Were it written =(warning . markdown-remark)=, a single formatting error would swallow the prose report along with it.
Since remark now runs =write-good= over the prose (and more carefully, as it skips the code), the [[file:ha-org.org::*Writegood][write-good]] checker no longer lists =markdown-mode= among its modes, and the two do not double up.
*** Markdown and Polymode
Using [[https://polymode.github.io/][polymode]], lets add syntax coloring to Markdown code blocks similar to what we do with Org:
#+begin_src emacs-lisp
(use-package polymode
:config
(define-hostmode poly-markdown-hostmode :mode 'markdown-mode)
(define-auto-innermode poly-markdown-fenced-code-innermode
:head-matcher (cons "^[ \t]*\\(```{?[[:alpha:]].*\n\\)" 1)
:tail-matcher (cons "^[ \t]*\\(```\\)[ \t]*$" 1)
:mode-matcher (cons "```[ \t]*{?\\(?:lang *= *\\)?\\([^ \t\n;=,}]+\\)" 1)
:head-mode 'host
:tail-mode 'host)
(define-polymode poly-markdown-mode
:hostmode 'poly-markdown-hostmode
:innermodes '(poly-markdown-fenced-code-innermode))
:mode ((rx ".md" string-end) . poly-markdown-mode))
#+end_src
** ReStructured Text
Support for [[https://docutils.sourceforge.io/rst.html][reStructuredText]] is [[https://www.emacswiki.org/emacs/reStructuredText][well supported]] in Emacs.
#+begin_src emacs-lisp
(use-package rst
:config
(when (and (display-graphic-p) (boundp 'ha-fixed-font))
(set-face-attribute 'rst-literal nil :font ha-fixed-font)))
#+end_src
** Docker
Edit =Dockerfiles= with the [[https://github.com/spotify/dockerfile-mode][dockerfile-mode]] project:
#+BEGIN_SRC emacs-lisp
(use-package dockerfile-mode
:mode (rx string-start "Dockerfile")
:config
(make-local-variable 'docker-image-name)
(defvaralias 'docker-image-name 'dockerfile-image-name nil)
(ha-local-leader :keymaps 'dockerfile-mode-map
"b" '("build" . dockerfile-build-buffer)
"B" '("build no cache" . dockerfile-build-no-cache-buffer)
"t" '("insert build tag" . ha-dockerfile-build-insert-header))
(defun ha-dockerfile-build-insert-header (image-name)
"Prepends the default Dockerfile image name at the top of a file."
(interactive "sDefault image name: ")
(save-excursion
(goto-char (point-min))
(insert (format "## -*- dockerfile-image-name: \"%s\" -*-" image-name))
(newline))))
#+END_SRC
/Control/ Docker from Emacs using the [[https://github.com/Silex/docker.el][docker.el]] project:
#+BEGIN_SRC emacs-lisp
(use-package docker
:commands docker
:config
(ha-leader "a d" 'docker))
#+END_SRC
Unclear whether I want to Tramp into a running container:
#+BEGIN_SRC emacs-lisp :tangle no
(use-package docker-tramp
:defer t
:after docker)
#+END_SRC
** Shell Scripts
While I don't like writing them, I can't get away from them. Check out the goodies in [[https://www.youtube.com/watch?v=LTC6SP7R1hA&t=5s][this video]].
While filename extensions work fine most of the time, I don't like to pre-pend =.sh= to the shell scripts I write, and instead, would like to associate =shell-mode= with all files in a =bin= directory:
#+begin_src emacs-lisp
(use-package sh-mode
:straight (:type built-in)
:mode (rx (or (seq ".sh" eol)
"/bin/"))
:init
(setq sh-basic-offset 4
sh-indentation 4)
:config
(ha-auto-insert-file (rx (or (seq ".sh" eol)
"/bin/"))
"sh-mode.sh")
:hook
(after-save . executable-make-buffer-file-executable-if-script-p))
#+end_src
*Note:* we make the script /executable/ by default. See [[https://emacsredux.com/blog/2021/09/29/make-script-files-executable-automatically/][this essay]] for details, but it turns on the executable bit if the script has a shebang at the top of the file.
The [[https://www.shellcheck.net/][shellcheck]] project integrates with [[Flycheck]]. First, install the executable into the system, for instance, on a Mac:
#+begin_src sh
brew install shellcheck
#+end_src
And we can enable it:
#+begin_src emacs-lisp
(flycheck-may-enable-checker 'sh-shellcheck)
#+end_src
Place the following /on a line/ before a shell script warning to ignore it:
#+begin_src sh
# shellcheck disable=SC2116,SC2086
#+end_src
See [[https://github.com/koalaman/shellcheck/wiki/Ignore][this page]] for details.
Integration with the [[https://github.com/bash-lsp/bash-language-server][Bash LSP implementation]]. First, install that too:
#+begin_src sh
brew install bash-language-server
#+end_src
*** Fish Shell
I think the [[https://fishshell.com/][fish shell]] is an interesting experiment (and I appreciate the basics that come with [[https://github.com/emacsmirror/fish-mode][fish-mode]]).
#+begin_src emacs-lisp
(use-package fish-mode
:mode (rx ".fish" eol)
:config
(ha-auto-insert-file (rx ".fish") "fish-mode.sh")
:hook
(fish-mode . (lambda () (add-hook 'before-save-hook 'fish_indent-before-save))))
#+end_src
* Technical Artifacts :noexport:
Provide a name to =require= this code.
#+begin_src emacs-lisp :exports none
(provide 'ha-programming)
;;; ha-programming.el ends here
#+end_src
Before you can build this on a new system, make sure that you put the cursor over any of these properties, and hit: ~C-c C-c~
#+description: A literate programming file for helping me program.
#+property: header-args:sh :tangle no
#+property: header-args:emacs-lisp :tangle yes
#+property: header-args :results none :eval no-export :comments no mkdirp yes
#+options: num:nil toc:t todo:nil tasks:nil tags:nil date:nil
#+options: skip:nil author:nil email:nil creator:nil timestamp:nil
#+infojs_opt: view:nil toc:t ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js