1092 lines
51 KiB
Org Mode
1092 lines
51 KiB
Org Mode
#+TITLE: General Programming Configuration
|
||
#+AUTHOR: Howard X. Abrams
|
||
#+DATE: 2020-10-26
|
||
|
||
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:
|
||
;; ~/other/hamacs/general-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.
|
||
** direnv
|
||
Farm off commands into /virtual environments/:
|
||
#+begin_src emacs-lisp
|
||
(use-package direnv
|
||
:init
|
||
(setq direnv--executable "/usr/local/bin/direnv"
|
||
direnv-always-show-summary t
|
||
direnv-show-paths-in-summary t)
|
||
:config
|
||
(direnv-mode))
|
||
#+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
|
||
** Flycheck
|
||
Why use [[https://www.flycheck.org/][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.
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package 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
|
||
** Documentation
|
||
I’m interested in using [[https://devdocs.io/][devdocs]] instead, which is similar, but keeps it all /inside/ Emacs (and works on my Linux system). 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-lookup)
|
||
|
||
:config
|
||
(ha-local-leader :keymaps 'prog-mode-map
|
||
"d" '(:ignore t :which-key "docs")
|
||
"d e" '("eldoc" . eldoc)
|
||
"d d" '("open" . devdocs-lookup)
|
||
"d p" '("peruse" . devdocs-peruse)
|
||
"d i" '("install" . devdocs-install)
|
||
"d u" '("update" . devdocs-update-all)
|
||
"d x" '("uninstall" . devdocs-delete)
|
||
"d s" '("search" . devdocs-search)))
|
||
#+end_src
|
||
|
||
The [[https://github.com/blahgeek/emacs-devdocs-browser][devdocs-browser]] project acts similar, but with slightly different command names. Its advantage is that it allows for downloading docs and having it available offline, in fact, you can’t search for a function, until you download its pack. This is slightly faster because of this.
|
||
#+begin_src emacs-lisp :tangle no
|
||
(use-package devdocs-browser
|
||
:general (:states 'normal "gD" 'devdocs-browser-open)
|
||
|
||
:config
|
||
(ha-local-leader :keymaps 'prog-mode-map
|
||
"d" '(:ignore t :which-key "docs")
|
||
"d d" '("open" . devdocs-browser-open)
|
||
"d D" '("open in" . devdocs-browser-open-in)
|
||
"d l" '("list" . devdocs-browser-list-docs)
|
||
"d u" '("update" . devdocs-browser-update-docs)
|
||
"d i" '("install" . devdocs-browser-install-doc)
|
||
"d x" '("uninstall" . devdocs-browser-uninstall-doc)
|
||
"d U" '("upgrade" . devdocs-browser-upgrade-doc)
|
||
"d o" '("download" . devdocs-browser-download-offline-data)
|
||
"d O" '("remove download" . devdocs-browser-remove-offline-data)))
|
||
#+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= doesn’t 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 we’ll 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
|
||
** 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:
|
||
#+begin_src emacs-lisp
|
||
(evil-define-key '(normal insert emacs) prog-mode-map
|
||
(kbd "M-h") 'beginning-of-defun
|
||
(kbd "M-l") 'beginning-of-next-defun)
|
||
#+end_src
|
||
But one of those functions doesn’t 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
|
||
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-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
|
||
#+end_src
|
||
|
||
The =npm install= /usually/ works, but I may work on some sort of various process, for instance:
|
||
#+begin_src sh
|
||
for TSS in ~/src/tree-sitter-*
|
||
do
|
||
cd $TSS
|
||
npm install || cargo build || make install # Various build processes!?
|
||
done
|
||
#+end_src
|
||
At this point, we can now parse stuff using: =tree-sitter parse <source-code-file>=
|
||
|
||
However, Emacs already has the ability to download and install grammars, so following instructions from Mickey Petersen’s essay on [[https://www.masteringemacs.org/article/combobulate-structured-movement-editing-treesitter][using Tree-sitter with Combobulate]]:
|
||
#+begin_src emacs-lisp
|
||
(when (string-search "TREE_SITTER" system-configuration-features)
|
||
(use-package treesit
|
||
:straight (:type built-in)
|
||
:preface
|
||
(defun mp-setup-install-grammars ()
|
||
"Install Tree-sitter grammars if they are absent."
|
||
(interactive)
|
||
(sit-for 10)
|
||
(dolist (grammar
|
||
'((bash "https://github.com/tree-sitter/tree-sitter-bash")
|
||
(make "https://github.com/alemuller/tree-sitter-make")
|
||
(css "https://github.com/tree-sitter/tree-sitter-css")
|
||
(json "https://github.com/tree-sitter/tree-sitter-json")
|
||
(html "https://github.com/tree-sitter/tree-sitter-html")
|
||
;; (javascript "https://github.com/tree-sitter/tree-sitter-javascript" "master" "src")
|
||
(python "https://github.com/tree-sitter/tree-sitter-python")
|
||
(ruby "https://github.com/tree-sitter/tree-sitter-ruby")
|
||
(yaml "https://github.com/ikatyang/tree-sitter-yaml")))
|
||
(add-to-list 'treesit-language-source-alist grammar)
|
||
(treesit-install-language-grammar (car grammar))))
|
||
|
||
;; Optional, but recommended. Tree-sitter enabled major modes are
|
||
;; distinct from their ordinary counterparts.
|
||
;;
|
||
;; 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 '((css-mode . css-ts-mode)
|
||
(json-mode . json-ts-mode)
|
||
;; (makefile-mode . makefile-ts-mode)
|
||
(python-mode . python-ts-mode)
|
||
(ruby-mode . ruby-ts-mode)
|
||
(sh-mode . bash-ts-mode)
|
||
(yaml-mode . yaml-ts-mode)))
|
||
(add-to-list 'major-mode-remap-alist mapping))
|
||
|
||
:config
|
||
(mp-setup-install-grammars)))
|
||
#+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 Petersen’s 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 (string-search "TREE_SITTER" system-configuration-features)
|
||
(use-package combobulate
|
||
:straight (:host github :repo "mickeynp/combobulate")
|
||
:after treesit
|
||
:hook ((css-ts-mode . combobulate-mode)
|
||
(json-ts-mode . combobulate-mode)
|
||
(python-ts-mode . combobulate-mode)
|
||
(yaml-ts-mode . combobulate-mode))))
|
||
#+end_src
|
||
|
||
I can create a /helper function/ to allow me to jump to various types of—well, /types/:
|
||
#+begin_src emacs-lisp
|
||
(when (string-search "TREE_SITTER" system-configuration-features)
|
||
(use-package combobulate
|
||
:config
|
||
(defun ha-comb-jump (&rest tree-sitter-types)
|
||
"Use `avy' to jump to a particular type of element.6 "
|
||
(lexical-let ((types tree-sitter-types))
|
||
(lambda ()
|
||
(interactive)
|
||
(with-navigation-nodes (:nodes types)
|
||
(combobulate-avy-jump)))))))
|
||
#+end_src
|
||
|
||
Now, I can create an /interface/ of keystrokes to jump around like a boss:
|
||
#+begin_src emacs-lisp
|
||
(when (string-search "TREE_SITTER" system-configuration-features)
|
||
(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-comb-jump "string"))
|
||
"g j c" `("comments" . ,(ha-comb-jump "comment"))
|
||
"g j i" `("conditionals" . ,(ha-comb-jump "conditional_expression" "if_statement"
|
||
"if_clause" "else_clause" "elif_clause" ))
|
||
"g j l" `("loops" . ,(ha-comb-jump "for_statement" "for_in_clause" "while_statement"
|
||
"list_comprehension" "dictionary_comprehension" "set_comprehension"))
|
||
"g j f" '("functions" . combobulate-avy-jump-defun))))
|
||
#+end_src
|
||
|
||
Mickey’s interface is the [[help:combobulate][combobulate]] function (or ~C-c o o~), but mine is more /evil/.
|
||
|
||
*** 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
|
||
(when (string-search "TREE_SITTER" system-configuration-features)
|
||
(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 "i" (evil-textobj-tree-sitter-get-textobj "conditional.outer"))
|
||
(define-key evil-inner-text-objects-map "i" (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—doesn’t work:
|
||
#+begin_src emacs-lisp :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)
|
||
|
||
;; Remove this now that https://github.com/jacktasia/dumb-jump/issues/338
|
||
;; (defun evil-set-jump-args (&rest ns) (evil-set-jump))
|
||
;; (advice-add 'dumb-jump-goto-file-line :before #'evil-set-jump-args)
|
||
|
||
(ha-local-leader :keymaps 'prog-mode-map
|
||
"s" '(:ignore t :which-key "search")
|
||
"s s" '("search" . xref-find-apropos)
|
||
"s d" '("definitions" . xref-find-definitions)
|
||
"s o" '("other window" . xref-find-definitions-other-window)
|
||
"s r" '("references" . xref-find-references)
|
||
"s b" '("back" . xref-go-back)
|
||
"s f" '("forward" . xref-go-forward))
|
||
|
||
: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 Evil’s. While comparable goals, they are behave different. Let’s 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]] … doesn’t 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 don’t have heavy IDE requirements, I am finding that [[eglot]] to be simpler.
|
||
*** LSP
|
||
#+begin_src emacs-lisp
|
||
(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)))
|
||
#+end_src
|
||
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 [[https://emacs-lsp.github.io/lsp-mode/page/keybindings/][this page]] for the default keybindings.
|
||
*** UI
|
||
The [[https://github.com/emacs-lsp/lsp-ui][lsp-ui]] project offers much of the display and interface to LSP. Seems to make the screen cluttered.
|
||
#+begin_src emacs-lisp
|
||
(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))
|
||
#+end_src
|
||
*** Treemacs
|
||
#+begin_src emacs-lisp
|
||
(use-package lsp-treemacs
|
||
:commands lsp-treemacs-errors-list
|
||
:bind
|
||
(:map prog-mode-map
|
||
("s-)" . treemacs))
|
||
(:map treemacs-mode-map
|
||
("s-)" . treemacs))
|
||
:config
|
||
(lsp-treemacs-sync-mode 1))
|
||
#+end_src
|
||
*** Company Completion
|
||
The [[https://github.com/tigersoldier/company-lsp][company-lsp]] offers a [[http://company-mode.github.io/][company]] completion backend for [[https://github.com/emacs-lsp/lsp-mode][lsp-mode]]:
|
||
|
||
#+begin_src emacs-lisp :tangle no
|
||
(use-package company-lsp
|
||
:config
|
||
(push 'company-lsp company-backends))
|
||
#+end_src
|
||
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.
|
||
|
||
*** iMenu
|
||
The [[https://github.com/emacs-lsp/lsp-ui/blob/master/lsp-ui-imenu.el][lsp-imenu]] project offers a =lsp-ui-imenu= function for jumping to functions:
|
||
|
||
#+begin_src emacs-lisp :tangle no
|
||
(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))
|
||
#+end_src
|
||
*** Display Configuration
|
||
Using the [[https://github.com/seagle0128/doom-modeline][Doom Modeline]] to add notifications:
|
||
#+begin_src emacs-lisp
|
||
(use-package doom-modeline
|
||
:config
|
||
(setq doom-modeline-lsp t
|
||
doom-modeline-env-version t))
|
||
#+end_src
|
||
** Function Call Notifications
|
||
As I've mentioned [[http://www.howardism.org/Technical/Emacs/beep-for-emacs.html][on my website]], I've created a [[file:~/website/Technical/Emacs/beep-for-emacs.org][beep function]] that notifies when long running processes complete.
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package alert
|
||
:init
|
||
(setq alert-default-style
|
||
(if (ha-running-on-macos?)
|
||
'osx-notifier
|
||
'libnotify)))
|
||
|
||
(use-package beep
|
||
:straight nil ; Already in the load-path
|
||
:hook (after-init . (lambda () (beep--when-finished "Emacs has started." "Eemacs has started")))
|
||
:config
|
||
(dolist (func '(org-publish
|
||
org-publish-all
|
||
org-publish-project
|
||
compile
|
||
shell-command))
|
||
(advice-add func :around #'beep-when-runs-too-long)))
|
||
#+end_src
|
||
While that code /advices/ the publishing and compile commands, I may want to add more.
|
||
** 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
|
||
(use-package iedit
|
||
:config
|
||
(ha-leader "s e" '("iedit" . iedit-mode)))
|
||
#+end_src
|
||
** Commenting
|
||
I like =comment-dwim= (~M-;~), and I like =comment-box=, but I have an odd personal style that I like to codify:
|
||
|
||
#+begin_src emacs-lisp
|
||
(defun ha-comment-line (&optional start end)
|
||
"Comment a line or region with a block-level format.
|
||
Calls `comment-region' with START and END set to the region or
|
||
the start and end of the line."
|
||
(interactive)
|
||
(when (or (null start) (not (region-active-p)))
|
||
(setq start (line-beginning-position))
|
||
(setq end (line-end-position)))
|
||
(save-excursion
|
||
(narrow-to-region start end)
|
||
(upcase-region start end)
|
||
(goto-char (point-min))
|
||
(insert "------------------------------------------------------------------------\n")
|
||
(goto-char (point-max))
|
||
(insert "\n------------------------------------------------------------------------")
|
||
(comment-region (point-min) (point-max))
|
||
(widen)))
|
||
#+end_src
|
||
And a keybinding:
|
||
#+begin_src emacs-lisp
|
||
(ha-local-leader :keymaps 'prog-mode-map
|
||
"c" '(:ignore t :which-key "comment")
|
||
"c l" '("comment line" . ha-comment-line))
|
||
#+end_src
|
||
** 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" '("last s-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 don’t 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
|
||
(use-package unicode-fonts
|
||
:config
|
||
(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 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:
|
||
#+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
|
||
|
||
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 project’s root and get targets from a =Makefile=, etc. We’ll 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 (projectile-project-root)))
|
||
;; 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 (projectile-project-root)))
|
||
(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 (projectile-project-name)))
|
||
|
||
;; (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 (projectile-project-name)))
|
||
|
||
(let ((command (replace-regexp-in-string rx-compile-to-eshell "" full-command)))
|
||
(ha-eshell-send command project-dir)))
|
||
#+end_src
|
||
And let’s 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 people’s 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
|
||
Let’s 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
|
||
While 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, I’ll deal with what is bundled now.
|
||
** Markdown
|
||
All the READMEs and other documentation use [[https://jblevins.org/projects/markdown-mode/][markdown-mode]].
|
||
#+begin_src emacs-lisp
|
||
(use-package markdown-mode
|
||
:straight (:host github :repo "jrblevin/markdown-mode")
|
||
:mode ((rx ".md" string-end) . gfm-mode)
|
||
:init (setq markdown-command "multimarkdown")
|
||
:general
|
||
(:states 'normal :no-autoload t :keymaps 'markdown-mode-map
|
||
", l" '("insert link" . markdown-insert-link)
|
||
;; SPC u 3 , h for a third-level header:
|
||
", h" '("insert header" . markdown-insert-header-dwim)
|
||
", e" '("export" . markdown-export)
|
||
", p" '("preview" . markdown-export-and-preview)))
|
||
#+end_src
|
||
Note that the markdown-specific commands use the ~C-c C-c~ and ~C-c C-s~ prefixes.
|
||
|
||
Using [[https://polymode.github.io/][polymode]], let’s 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
|
||
** YAML
|
||
Doing a lot of [[https://github.com/yoshiki/yaml-mode][YAML work]], but this projeDoing a lot of [[https://github.com/yoshiki/yaml-mode][YAML work]], but this =yaml-mode= project needs a new maintainer, so I’ve switch to [[https://github.com/zkry/yaml-pro][yaml-pro]] that is now based on Tree Sitter. Let’s make sure the Tree-Sitter version works:
|
||
#+begin_src emacs-lisp
|
||
(if (string-search "TREE_SITTER" system-configuration-features)
|
||
(progn
|
||
(use-package yaml-ts-mode
|
||
:mode ((rx ".y" (optional "a") "ml" string-end)
|
||
(rx (optional ".") "yamllint"))
|
||
:hook (yaml-ts-mode . display-line-numbers-mode))
|
||
|
||
(use-package yaml-pro
|
||
:straight (:host github :repo "zkry/yaml-pro")
|
||
:after yaml-ts-mode
|
||
:hook (yaml-ts-mode . yaml-pro-ts-mode)))
|
||
|
||
(use-package yaml-mode
|
||
:mode (rx ".y" (optional "a") "ml" string-end)
|
||
(rx (optional ".") "yamllint")
|
||
:hook (yaml-mode . display-line-numbers-mode)))
|
||
#+end_src
|
||
|
||
This comes with a list of nice refactoring features that we can attach to the local leader:
|
||
#+begin_src emacs-lisp
|
||
(when (string-search "TREE_SITTER" system-configuration-features)
|
||
(use-package yaml-pro
|
||
:config
|
||
(ha-local-leader :keymaps 'yaml-pro-ts-mode-map
|
||
"u" '("up" . yaml-pro-ts-up-level) ; C-c C-u
|
||
"j" '("next" . yaml-pro-ts-next-subtree) ; C-c C-n
|
||
"k" '("previous" . yaml-pro-ts-prev-subtree) ; C-c C-p
|
||
|
||
"m" '("mark tree" . yaml-pro-ts-mark-subtree) ; C-c C-@
|
||
"d" '("kill subtree" . yaml-pro-kill-subtree) ; C-c C-x C-w
|
||
"y" '("paste tree" . yaml-pro-ts-paste-subtree) ; C-c C-x C-y
|
||
|
||
"'" '("edit" . yaml-pro-edit-ts-scalar) ; C-c '
|
||
|
||
"r" '(:ignore t :which-key "refactor")
|
||
"r k" '("move up" . yaml-pro-ts-move-subtree-up) ; s-↑
|
||
"r j" '("move down" . yaml-pro-ts-move-subtree-down) ; s-↓
|
||
|
||
;; "r " '("" . yaml-pro-ts-meta-return) ; M-<return>
|
||
"r c" '("convolute" . yaml-pro-ts-convolute-tree) ; M-?
|
||
"r i" '("indent" . yaml-pro-ts-indent-subtree) ; C-c >
|
||
"r o" '("outdent" . yaml-pro-ts-unindent-subtree)))) ; C-c <
|
||
#+end_src
|
||
Seems like I need a predicate to check for the existence of Tree Sitter support?
|
||
|
||
Note that these packages need the following to run properly:
|
||
#+begin_src sh
|
||
pip install yamllint
|
||
#+end_src
|
||
** Jinja2
|
||
A lot of projects (like Ansible and Zuul) uses [[https://jinja.palletsprojects.com][Jinja2]] with YAML, so we install the [[https://github.com/paradoxxxzero/jinja2-mode][jinja2-mode]]:
|
||
#+begin_src emacs-lisp
|
||
(use-package jinja2-mode
|
||
:mode (rx ".j2" string-end))
|
||
#+end_src
|
||
|
||
Jinja is a /templating/ system that integrates /inside/ formats like JSON, HTML or YAML.
|
||
The [[https://polymode.github.io/][polymode]] project /glues/ modes like [[https://github.com/paradoxxxzero/jinja2-mode][jinja2-mode]] to [[https://github.com/yoshiki/yaml-mode][yaml-mode]].
|
||
|
||
I adapted this code from the [[https://github.com/emacsmirror/poly-ansible][poly-ansible]] project:
|
||
#+begin_src emacs-lisp
|
||
(use-package polymode
|
||
:config
|
||
(define-hostmode poly-yaml-hostmode :mode 'yaml-ts-mode)
|
||
(defcustom pm-inner/jinja2
|
||
(pm-inner-chunkmode :mode #'jinja2-mode
|
||
:head-matcher "{[%{#][+-]?"
|
||
:tail-matcher "[+-]?[%}#]}"
|
||
:head-mode 'body
|
||
:tail-mode 'body
|
||
:head-adjust-face t)
|
||
"Jinja2 chunk."
|
||
:group 'innermodes
|
||
:type 'object)
|
||
(define-polymode poly-yaml-jinja2-mode
|
||
:hostmode 'poly-yaml-hostmode
|
||
:innermodes '(pm-inner/jinja2))
|
||
|
||
:mode ((rx ".y" (optional "a") "ml" string-end) . poly-yaml-jinja2-mode))
|
||
#+end_src
|
||
** Ansible
|
||
Do I consider all YAML files an Ansible file needing [[https://github.com/k1LoW/emacs-ansible][ansible-mode]]? Maybe we just have a toggle for when we want the Ansible feature.
|
||
#+begin_src emacs-lisp
|
||
(use-package ansible
|
||
:mode (rx (or "playbooks" "roles"))
|
||
:config
|
||
(setq ansible-vault-password-file "~/.ansible-vault-passfile")
|
||
(ha-leader "t y" 'ansible))
|
||
#+end_src
|
||
|
||
The [[help:ansible-vault-password-file][ansible-vault-password-file]] variable needs to change /per project/, so let’s use the =.dir-locals.el= file, for instance:
|
||
#+begin_src emacs-lisp :tangle no
|
||
((nil . ((ansible-vault-password-file . "playbooks/.vault-password"))))
|
||
#+end_src
|
||
|
||
The YAML files get access Ansible’s documentation using the [[https://github.com/emacsorphanage/ansible-doc][ansible-doc]] project:
|
||
#+begin_src emacs-lisp
|
||
(use-package ansible-doc
|
||
:hook (ansible-mode . ansible-doc-mode)
|
||
:config
|
||
(ha-local-leader :keymaps 'ansible-key-map
|
||
"d" '(:ignore t :which-key "docs")
|
||
"d d" 'ansible-doc))
|
||
#+end_src
|
||
|
||
|
||
Can we integrate Ansible with LSP using [[https://github.com/ansible/ansible-language-server][ansible-language-server]] project (see [[https://emacs-lsp.github.io/lsp-mode/page/lsp-ansible/][this documentation]])?
|
||
|
||
Using =npm= to install the program:
|
||
#+begin_src sh
|
||
npm installl -g @ansible/ansible-language-server
|
||
#+end_src
|
||
But … will I get some use out of this? I’ll come back to it later.
|
||
** 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 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))
|
||
#+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 yes
|
||
#+PROPERTY: header-args :results none :eval no-export :comments no mkdirp yes
|
||
|
||
#+OPTIONS: num:nil toc:nil todo:nil tasks:nil tags:nil date:nil
|
||
#+OPTIONS: skip:nil author:nil email:nil creator:nil timestamp:nil
|
||
#+INFOJS_OPT: view:nil toc:nil ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js
|