General Programming Configuration
Table of Contents
A literate programming file for helping me program.
Introduction
Configuration for programming interfaces and workflows that behave similarly.
General
The following work for all programming languages.
Virtual Environments with direnv
Farm off commands into virtual environments:
(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))
Displaying Code
Displaying Line Numbers
For all programming languages, I would like to now default to absolute line numbers, and turn off the Mixed Pitch feature that seems to come along for the ride:
(use-package emacs :config (defun ha-prog-mode-config () "Configure the `prog-mode'" (setq display-line-numbers t) (mixed-pitch-mode 0)) :hook (prog-mode . ha-prog-mode-config))
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 hide-show feature:
(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))
Note that hide-show
doesn’t work with complex YAML files. The 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.
(use-package origami :init (setq origami-fold-replacement "⤵") :hook (prog-mode . origami-mode))
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 vimish-fold (and its cousin, evil-vimish-fold) and we’ll see if I need those.
Smart Parenthesis
We need to make sure we keep the smartparens project always in strict mode, because who wants to worry about paren-matching:
(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))
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.
(use-package auto-highlight-symbol :config (setq ahs-idle-interval 0.1) (set-face-attribute ahs-face nil :foreground nil :background nil :weight 'ultra-bold :slant 'italic) (set-face-attribute ahs-plugin-default-face nil :foreground nil :background nil :weight 'bold :slant 'normal))
Instead of calling global-auto-highlight-symbol-mode
, we should just hook it to the prog-mode
:
(use-package auto-highlight-symbol :hook ((prog-mode . auto-highlight-symbol-mode)))
Similarly, the symbol-overlay project highlights instances of symbols, but like iedit
creates a keymap allowing manipulation of the symbols. The workflow is:
SPC t s s
to highlight the symbol at point.n
andp
to move from symbol to symbol.- Quitting the menu involves one of these:
q
to leave point at spote
to return to previous cursor placementx
to un-highlight all symbols
SPC t s s
to highlight another symbol at point.N
andP
to move to a different symbol.r
to rename symbol (likeiedit
)
(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)))
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 this essay by Álvaro Ramírez, I’ve been thinking of ways to connect services together. In my case, I am not sure I need 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.
(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)))
Spell Checking Comments
The flyspell-prog-mode checks for misspellings in comments.
(use-package flyspell :hook (prog-mode . flyspell-prog-mode))
Linting with Flycheck
Why use flycheck over the built-in flymake
? Speed used to be the advantage, but I’m now pushing much of this to LSP, so speed is less of an issue. What about when I am not using LSP? Also, since I’ve hooked grammar checkers, I need this with global keybindings.
(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)))
Language Documentation
Used to use the Dash project for searching documentation associated with a programming language, but that hardly worked on my Linux systems.
I’m interested in using 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 devdocs project is active, and seems to work well. Its advantage is a special mode for moving around the documentation.
(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")))))
The devdocs-lookup
command attempts to guess which documentation it should display based on the mode, but if I’m editing YAML files, I actually want to pull up the Ansible documentation, and probably the Jinja ones too.
(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)))
Navigation
Move by Functions
The mark-paragraph
and downcase-word
isn’t very useful in a programming context, and makes more sense to use them to jump around function-by-function:
; (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))
But one of those functions doesn’t exist:
(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))
Tree Sitter
I’m curious about the new Tree Sitter feature now built into Emacs 29. After following along with Mickey Petersen’s Getting Started with Tree Sitter guide, I’ve concluded I currently don’t need this feature. I’m leaving the code here, but adding a :tangle no
to all the blocks until I’m ready to re-investigate.
- Operating System Part
Install the binary for the tree-sitter project. For instance:
brew install tree-sitter npm # Since most support packages need that too.
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 config.json file:tree-sitter init-config
Normally, you would need to add all the projects to directory clones in
~/src
, e.g.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-css https://github.com/tree-sitter/tree-sitter-json 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/ikatyang/tree-sitter-yaml https://github.com/Wilfred/tree-sitter-elisp EOL
Seems that Docker is a bit of an odd-ball:
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
In most cases,the
npm install
usually works, but I may work on some sort of various process, for instance:for TSS in ~/src/tree-sitter-* do cd $TSS NAME=$(pwd | sed 's/.*-//') git pull origin npm install || cargo build || make install # Various build processes!? echo "Do we need to copy the library into ~/.emacs.d/tree-sitter/$NAME ?" # if [ "$(uname -o)" = "Darwin" ] # then # cp libtree-sitter-$NAME.dylib ~/.emacs.d/tree-sitter # else # cp libtree-sitter-$NAME.so ~/.emacs.d/tree-sitter # fi done
At this point, we can now parse stuff using:
tree-sitter parse <source-code-file>
- Emacs Part
However, Emacs already has the ability to download and install grammars, so following instructions from Mickey Petersen’s essay on using Tree-sitter with Combobulate:
(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") ;; (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/ikatyang/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)))
And enable the languages:
(when (treesit-available-p) (use-package tree-sitter-langs :after treesit :config (global-tree-sitter-mode)))
Combobulate
I like , but can we extend that to other languages generally? After reading Mickey Petersen’s essay, Combobulate project, I decided to try out his combobulate package. Of course, this can only work with the underlying tooling supplied by the Tree Sitter →
(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) ) ))
Now, I can create an interface of keystrokes to jump around like a boss:
(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))))))
Mickey’s interface is the 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:
(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))))
Evil Text Object from Tree Sitter
With Emacs version 29, we get a better approach to parsing languages, and this means that our text objects can be better too with the evil-textobj-tree-sitter project:
(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"))))
Seems the macro, evil-textobj-tree-sitter-get-textobj
has a bug, so the following—which would have been easier to write—doesn’t work:
(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))))
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. ack, ag (aka, the Silver Searcher), ugrep and ripgrep, we should be able to use them. but I want to:
- Be in a function, and see its callers. For this, the rg-dwim function is my bread-and-butter.
- Be on a function, and jump to the definition. For this, I use dumb-jump, which uses the above utilities.
(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))
While I’m at it, let’s connect various g
sequence keys to xref-
interface functions:
(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)))
I have two different jumping systems, the Xref interface and Evil’s. While comparable goals, they are behave different. Let’s compare evil keybindings:
M-. |
g . |
xref-find-definitions (also g d for evil-goto-definition)† |
g > |
xref-find-definitions-other-window |
|
M-, |
g , |
xref-go-back (see xref-pop-marker-stack) |
C-M-, |
g < |
xref-go-forward (kinda like xref-find-definitions ) |
M-? |
g / |
xref-find-references to go from definition to code calls‡ |
g ? |
xref-find-references-and-replace could be more accurate than iEdit. | |
C-M-. |
g h |
xref-find-apropos … doesn’t work well without LSP |
C-TAB |
perform completion around point (also M-TAB ), see . |
† Prefix to prompt for the term \ ‡ If it finds more than one definition, Emacs displays the xref buffer, allowing you to select the definition.
Language Server Protocol (LSP) Integration
The 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 Jedi and the Emacs integration to Jedi (and duplicate everything for Ruby, and Clojure, and…).
Emacs has two LSP projects, and while I have used , but since I don’t have heavy IDE requirements, I am finding that to be simpler.
LSP
(use-package lsp-mode :commands (lsp lsp-deferred) :init ;; Let's make lsp-doctor happy with these settings: (setq gc-cons-threshold (* 100 1024 1024) read-process-output-max (* 1024 1024) company-idle-delay 0.0 ; Are thing fast enough to do this? lsp-keymap-prefix "s-m") :config (global-set-key (kbd "s-m") 'lsp) (ha-local-leader :keymaps 'prog-mode-map "w" '(:ignore t :which-key "lsp") "l" '(:ignore t :which-key "lsp") "ws" '("start" . lsp)) ;; The following leader-like keys, are only available when I have ;; started LSP, and is an alternate to Command-m: :general (:states 'normal :keymaps 'lsp-mode-map ", w r" '("restart" . lsp-reconnect) ", w b" '("events" . lsp-events-buffer) ", w e" '("errors" . lsp-stderr-buffer) ", w q" '("quit" . lsp-shutdown) ", w Q" '("quit all" . lsp-shutdown-all) ", l r" '("rename" . lsp-rename) ", l f" '("format" . lsp-format) ", l a" '("actions" . lsp-code-actions) ", l i" '("imports" . lsp-code-action-organize-imports) ", l d" '("doc" . lsp-lookup-documentation)) :hook ((lsp-mode . lsp-enable-which-key-integration)))
I will want to start adding commands under my ,
mode-specific key sequence leader, but in the meantime, all LSP-related keybindings are available under ⌘-m
. See this page for the default keybindings.
Using the Doom Modeline to add notifications:
(use-package doom-modeline :config (setq doom-modeline-lsp t doom-modeline-env-version t))
- UI
The lsp-ui project offers much of the display and interface to LSP. Seems to make the screen cluttered.
(use-package lsp-ui :commands lsp-ui-mode :config (setq lsp-ui-sideline-ignore-duplicate t lsp-ui-sideline-show-hover t lsp-ui-sideline-show-diagnostics t) :hook (lsp-mode . lsp-ui-mode))
Company Completion
The company-lsp offers a company completion backend for lsp-mode:
(use-package company-lsp :config (push 'company-lsp company-backends))
To options that might be interesting:
company-lsp-async
: When set to non-nil, fetch completion candidates asynchronously.company-lsp-enable-snippet
: Set it to non-nil if you want to enable snippet expansion on completion. Set it to nil to disable this feature.
LSP iMenu
The lsp-imenu project offers a lsp-ui-imenu
function for jumping to functions:
(use-package lsp-ui-imenu :straight nil :after lsp-ui :config (ha-local-leader :keymaps 'prog-mode-map "g" '(:ignore t :which-key "goto") "g m" '("imenu" . lsp-ui-imenu)) (add-hook 'lsp-after-open-hook 'lsp-enable-imenu))
General Code Editing
iEdit
While there are language-specific ways to rename variables and functions, iedit is often sufficient.
(use-package iedit :config (ha-leader "s e" '("iedit" . iedit-mode)))
While iedit
acts a little odd with Evil, the 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 you’ve selected the variable/symbol, and then type e
. Or, highlight normally, e.g. v i o
, and hit E
:
(when (fboundp 'evil-mode) (use-package evil-iedit-state :after iedit :general (:states 'visual "E" '("iedit" . evil-iedit-state/iedit-mode))))
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 (obviouslya
andi
do too) #
- highlights all the matching occurrences
F
- restricts to the current function
Case Conversion
The string-inflection project (see this overview) converts symbol variables to appropriate format for the mode. This replaces my home-brewed functions.
(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)))
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 eval-print-last-sexp, I would like a bit of formatting in order to keep the results in the file.
(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)))
Typical keybindings for all programming modes:
(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))
Ligatures
The idea of using math symbols for a programming languages keywords is cute, but can be confusing, so I use it sparingly:
(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)
Hopefully I can follow Mickey Petersen’s essay on getting full ligatures working, but right now, they don’t work on the Mac, and that is my current workhorse.
(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))
Until I can get Harfbuzz support on my Emacs-Plus build of Mac, the following work-around seems to mostly work:
(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))
The unicode-fonts package rejigs the internal tables Emacs uses to pick better fonts for unicode codepoint ranges.
(use-package unicode-fonts :config (ignore-errors (unicode-fonts-setup)))
Compiling
The compile function lets me enter a command to run, or I can search the history for a previous run. What it doesn’t 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:
((nil . ((compile-command . "make -k ") (compile-command-list . ("ansible-playbook playbooks/confluence_test.yml" "ansible-playbook playbooks/refresh_inventory.yml")))))
To make the compile-command-list
variable less risky, we need to declare it:
(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)))
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 project’s root and get targets from a Makefile
, etc. We’ll use helper functions I define later:
(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))))
My replacement to compile uses my new completing-read
function:
(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)))))
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:
(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)))
And what about sending the command to Eshell as well?
(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)))
And let’s add it to the Project leader:
(ha-leader "p C" 'ha-project-compile)
Note that p c
(to call recompile) should still work.
Other people’s projects:
- makefile-executor.el
- works only with Makefiles
- imake
- works only with Makefiles that are formatted with a
help:
target - Taskrunner project
- requires ivy or helm, but perhaps I could use the underlying infrastructure to good ol’ completing-read
Note: Someday I may want to convert my Makefile
projects to Taskfile.
Makefile Completion
This magic script is what Bash uses for completion when you type make
and hit the TAB:
make -qRrp : 2> /dev/null | awk -F':' '/^[a-zA-Z0-9][^$#\\/\\t=]*:([^=]|$)/ {split($1,A,/ /);for(i in A)print A[i]}'
Which makes it easy to get a list of completions for my compile function:
(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 -qRrp : 2> /dev/null | awk -F':' '/^[a-zA-Z0-9][^$#\\/\\t=]*:([^=]|$)/ {split($1,A,/ /);for(i in A)print A[i]}'"))))
Python Tox Completion
Let’s just grab the environments to run:
(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"))))
Languages
Simple to configure languages go here. More advanced languages go into their own files… eventually.
Configuration Files
So many configuration files to track:
(use-package conf-mode :mode (("\\.conf\\'" . conf-space-mode) ("\\.repo\\'" . conf-unix-mode) ("\\.setup.*\\'" . conf-space-mode)))
JSON
While interested in the tree-sitter extensions for JSON, e.g. json-ts-mode
, that comes with Emacs 29, I’ll deal with what is bundled now.
However, what about taking a buffer of JSON data, and whittling it down with jq?
(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)
This means, that some data like:
{ "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 yawxway service image with name yawxway-howard.abrams-test and docker-dev-artifactory.workday.com/dev/yawxway-service:latest docker url in development folder", "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" }
I can type, , j
and then type .data.timeout.seconds
and end up with:
300
Markdown
Most project README
files and other documentation use markdown-mode. Note that the preview is based on multimarkdown
, when needs to be pre-installed, for instance:
brew install multimarkdown
Also, I like Markdown is look like a word processor, similarly to my org files:
(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)))
Note that the markdown-specific commands use the C-c C-c
and C-c C-s
prefixes.
Let’s make sure that markdown is proper using PyMarkdown. First, get the script installed globally:
pip install pymarkdown
And then we can use it. For some reason, the pymarkdown
(which I need to use from work) doesn’t seem to be part of the version of Flycheck available on Melpa, so…
(use-package markdown-mode :after flycheck :config (setq flycheck-markdown-pymarkdown-config ".pymarkdown.yml") (flycheck-may-enable-checker 'markdown-pymarkdown))
Ugh
(flycheck-def-config-file-var flycheck-markdown-pymarkdown-config markdown-pymarkdown nil :package-version '(flycheck . "34")) (flycheck-define-checker markdown-pymarkdown "Markdown checker using PyMarkdown. See URL `https://pypi.org/project/pymarkdownlnt/'." :command ("pymarkdown" (config-file "--config" flycheck-markdown-pymarkdown-config) "scan" source) :error-patterns ((error line-start (file-name) ":" line (? ":" column) ": " (id (one-or-more alnum)) ": " (message) line-end)) :error-filter (lambda (errors) (flycheck-sanitize-errors (flycheck-remove-error-file-names "(string)" errors))) :modes (markdown-mode gfm-mode))
Both the markdown-command
and the markdown-open-command
variables are called 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):
pandoc --to=html --from=gfm $*
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
Using polymode, let’s add syntax coloring to Markdown code blocks similar to what we do with Org:
(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))
ReStructured Text
Support for reStructuredText is well supported in Emacs.
(use-package rst :config (when (and (display-graphic-p) (boundp 'ha-fixed-font)) (set-face-attribute 'rst-literal nil :font ha-fixed-font)))
Docker
Edit Dockerfiles
with the dockerfile-mode project:
(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))))
Control Docker from Emacs using the docker.el project:
(use-package docker :commands docker :config (ha-leader "a d" 'docker))
Unclear whether I want to Tramp into a running container:
(use-package docker-tramp :defer t :after docker)
Shell Scripts
While I don’t like writing them, I can’t get away from them. Check out the goodies in 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:
(use-package sh-mode :straight (:type built-in) :mode (rx (or (seq ".sh" eol) "/bin/")) :init (setq sh-basic-offset 2 sh-indentation 2) :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))
Note: we make the script executable by default. See this essay for details, but it turns on the executable bit if the script has a shebang at the top of the file.
The shellcheck project integrates with . First, install the executable into the system, for instance, on a Mac:
brew install shellcheck
And we can enable it:
(flycheck-may-enable-checker 'sh-shellcheck)
Place the following on a line before a shell script warning to ignore it:
# shellcheck disable=SC2116,SC2086
See this page for details.
Integration with the Bash LSP implementation. First, install that too:
brew install bash-language-server
Fish Shell
I think the fish shell is an interesting experiment (and I appreciate the basics that come with fish-mode).
(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))))