a3e92b4f7f
The Tree Sitter just isn't changing anything for me, so I'm removing this code for the time being. I'll reinvestigate later.
1294 lines
60 KiB
Org Mode
1294 lines
60 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" . 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
|
||
I’m curious about the new [[https://emacs-tree-sitter.github.io/][Tree Sitter feature]] now [[https://lists.gnu.org/archive/html/emacs-devel/2022-11/msg01443.html][built into Emacs 29]]. After following along with Mickey Petersen’s [[https://www.masteringemacs.org/article/how-to-get-started-tree-sitter][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 [[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
|
||
|
||
Seems that Docker is a bit of an odd-ball:
|
||
#+begin_src sh
|
||
mkdir -p ~/src
|
||
git -C ~/src clone https://github.com/camdencheek/tree-sitter-dockerfile
|
||
make -C ~/src/tree-sitter-dockerfile && \
|
||
make -C ~/src/tree-sitter-dockerfile install
|
||
if [[ $(uname -n) = "Darwin" ]]
|
||
then
|
||
cp ~/src/tree-sitter-dockerfile/libtree-sitter-dockerfile.dylib \
|
||
~/.emacs.d/tree-sitter
|
||
else
|
||
cp ~/src/tree-sitter-dockerfile/libtree-sitter-dockerfile.so \
|
||
~/.emacs.d/tree-sitter
|
||
fi
|
||
#+end_src
|
||
|
||
In most cases,the =npm install= /usually/ works, but I may work on some sort of various process, for instance:
|
||
#+begin_src shell
|
||
for TSS in ~/src/tree-sitter-*
|
||
do
|
||
cd $TSS
|
||
NAME=$(pwd | sed 's/.*-//')
|
||
|
||
git pull origin
|
||
npm install || 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
|
||
#+end_src
|
||
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 [[https://www.masteringemacs.org/article/combobulate-structured-movement-editing-treesitter][using Tree-sitter with Combobulate]]:
|
||
#+begin_src emacs-lisp :tangle no
|
||
(when (string-search "TREE_SITTER" system-configuration-features)
|
||
(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)))
|
||
#+end_src
|
||
|
||
And enable the languages:
|
||
#+begin_src emacs-lisp :tangle no
|
||
(when (treesit-available-p)
|
||
(use-package tree-sitter-langs
|
||
:after treesit
|
||
:config
|
||
(global-tree-sitter-mode)))
|
||
#+end_src
|
||
*** Combobulate
|
||
I like [[file:ha-programming-elisp.org::*Clever Parenthesis][Clever Parenthesis]], but can we extend that to other languages generally? After reading Mickey 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 :tangle no
|
||
(when (treesit-available-p)
|
||
(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 :tangle no
|
||
(when (treesit-available-p)
|
||
(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 :tangle no
|
||
(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-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 :tangle no
|
||
(when (treesit-available-p)
|
||
(use-package evil-textobj-tree-sitter
|
||
:config
|
||
;; We need to bind keys to the text objects found at:
|
||
;; https://github.com/nvim-treesitter/nvim-treesitter-textobjects#built-in-textobjects
|
||
|
||
;; bind `function.outer`(entire function block) to `f` for use in things like `vaf`, `yaf`
|
||
(define-key evil-outer-text-objects-map "f" (evil-textobj-tree-sitter-get-textobj "function.outer"))
|
||
;; bind `function.inner`(function block without name and args) to `f` for use in things like `vif`, `yif`
|
||
(define-key evil-inner-text-objects-map "f" (evil-textobj-tree-sitter-get-textobj "function.inner"))
|
||
|
||
(define-key evil-outer-text-objects-map "c" (evil-textobj-tree-sitter-get-textobj "comment.outer"))
|
||
(define-key evil-inner-text-objects-map "c" (evil-textobj-tree-sitter-get-textobj "comment.inner"))
|
||
(define-key evil-outer-text-objects-map "u" (evil-textobj-tree-sitter-get-textobj "conditional.outer"))
|
||
(define-key evil-inner-text-objects-map "u" (evil-textobj-tree-sitter-get-textobj "conditional.inner"))
|
||
(define-key evil-outer-text-objects-map "b" (evil-textobj-tree-sitter-get-textobj "loop.outer"))
|
||
(define-key evil-inner-text-objects-map "b" (evil-textobj-tree-sitter-get-textobj "loop.inner"))))
|
||
#+end_src
|
||
|
||
Seems the macro, =evil-textobj-tree-sitter-get-textobj= has a bug, so the following—which would have been easier to write—doesn’t work:
|
||
#+begin_src emacs-lisp :tangle no :tangle no
|
||
(dolist (combo '(("f" "function.outer" "function.inner")
|
||
("b" "loop.outer" "loop.inner")
|
||
;; ...
|
||
("c" "comment.outer" "comment.inner")))
|
||
(destructuring-bind (key outer inner) combo
|
||
;; bind an outer (e.g. entire function block) for use in things like `vaf`, `yaf` combo
|
||
(define-key evil-outer-text-objects-map key (evil-textobj-tree-sitter-get-textobj outer))
|
||
;; bind an inner (e.g. function block without name and args) for use in things like `vif`, `yif`
|
||
(define-key evil-inner-text-objects-map key (evil-textobj-tree-sitter-get-textobj inner))))
|
||
#+end_src
|
||
*** dumb-jump
|
||
Once upon a time, we use to create a =TAGS= file that contained the database for navigating code bases, but with new faster versions of grep, e.g. [[https://beyondgrep.com][ack]], [[https://github.com/ggreer/the_silver_searcher][ag]] (aka, the Silver Searcher), [[https://github.com/Genivia/ugrep][ugrep]] and [[https://github.com/BurntSushi/ripgrep][ripgrep]], we should be able to use them. but I want to:
|
||
- Be in a function, and see its callers. For this, the [[help:rg-dwim][rg-dwim]] function is my bread-and-butter.
|
||
- Be on a function, and jump to the definition. For this, I use [[https://github.com/jacktasia/dumb-jump][dumb-jump]], which uses the above utilities.
|
||
|
||
#+begin_src emacs-lisp
|
||
(use-package dumb-jump
|
||
:config
|
||
(setq dumb-jump-prefer-searcher 'rg
|
||
xref-history-storage #'xref-window-local-history
|
||
xref-show-definitions-function #'xref-show-definitions-completing-read)
|
||
|
||
(add-hook 'xref-backend-functions #'dumb-jump-xref-activate)
|
||
|
||
;; 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
|
||
** Case Conversion
|
||
The [[https://github.com/akicho8/string-inflection][string-inflection]] project (see [[http://sodaware.sdf.org/notes/converting-to-snake-case-in-emacs/][this overview]]) converts symbol variables to /appropriate format/ for the mode. This replaces my home-brewed functions.
|
||
#+begin_src emacs-lisp
|
||
(use-package string-inflection
|
||
:general
|
||
(:states '(normal visual motion operator)
|
||
"z s" '("to snake case" . string-inflection-underscore)
|
||
"z S" '("to Snake Case" . string-inflection-upcase)
|
||
"z c" '("to camelCase" . string-inflection-lower-camelcase)
|
||
"z C" '("to CamelCase" . string-inflection-camelcase)
|
||
"z -" '("to kebab case" . string-inflection-kebab-case)
|
||
"z z" '("toggle snake/camel" . string-inflection-all-cycle)))
|
||
#+end_src
|
||
I would like to have this bound on the ~g~ sequence, but that is crowded.
|
||
|
||
Note that ~g u~ (for lower-casing stuff), and ~g U~ (for up-casing) requires /something/, for instance ~g U i o~ upper-cases the symbol at point. These functions, however, only work with a symbol (which is the typical case).
|
||
** 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" '("eval exp" . eval-last-sexp)
|
||
"e p" '("print s-exp" . ha-eval-print-last-sexp))
|
||
#+end_src
|
||
** Ligatures
|
||
The idea of using math symbols for a programming languages keywords is /cute/, but can be confusing, so I use it sparingly:
|
||
#+begin_src emacs-lisp
|
||
(defun ha-prettify-prog ()
|
||
"Extends the `prettify-symbols-alist' for programming."
|
||
(mapc (lambda (pair) (push pair prettify-symbols-alist))
|
||
'(("lambda" . "𝝀")
|
||
(">=" . "≥")
|
||
("<=" . "≤")
|
||
("!=" . "≠")))
|
||
(prettify-symbols-mode))
|
||
|
||
(add-hook 'prog-mode-hook 'ha-prettify-prog)
|
||
#+end_src
|
||
|
||
Hopefully I can follow [[https://www.masteringemacs.org/article/unicode-ligatures-color-emoji][Mickey Petersen's essay]] on getting full ligatures working, but right now, they 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.
|
||
|
||
However, what about taking a buffer of JSON data, and whittling it down with [[https://jqlang.github.io/jq/][jq]]?
|
||
#+begin_src emacs-lisp
|
||
(defun ha-json-buffer-to-jq (query)
|
||
"Runs JSON buffer with QUERY through an external `jq' program.
|
||
Attempts to find the first JSON object in the buffer, and limits
|
||
the data to that region. The `jq' program is the first found in
|
||
the standard path."
|
||
(interactive "sjq Query: ")
|
||
(let (s e)
|
||
(save-excursion
|
||
(if (region-active-p)
|
||
(setq s (region-beginning)
|
||
e (region-end))
|
||
(goto-char (point-min))
|
||
(unless (looking-at "{")
|
||
(re-search-forward "{")
|
||
(goto-char (match-beginning 0)))
|
||
(setq s (point))
|
||
(evil-jump-item)
|
||
(setq e (1+ (point))))
|
||
;; (narrow-to-region s e)
|
||
(shell-command-on-region s e (concat "jq " query) nil t "*jq errors*"))))
|
||
|
||
(ha-local-leader :keymaps '(js-json-mode-map json-ts-mode-map)
|
||
"j" 'ha-json-buffer-to-jq)
|
||
#+end_src
|
||
|
||
This means, that some data like:
|
||
#+begin_src json :tangle no
|
||
{
|
||
"common_id": "GMC|F2BADC23|64D52BF7|awardlateengine",
|
||
"data": {
|
||
"name": "Create And Wait for Service Image",
|
||
"description": "Creates a new Service Image using IMaaS",
|
||
"long_description": "This job creates a new 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"
|
||
}
|
||
#+end_src
|
||
|
||
I can type, ~, j~ and then type =.data.timeout.seconds= and end up with:
|
||
#+begin_src json
|
||
300
|
||
#+end_src
|
||
** Markdown
|
||
Most project =README= files and other documentation use [[https://jblevins.org/projects/markdown-mode/][markdown-mode]]. Note that the /preview/ is based on =multimarkdown=, when needs to be /pre-installed/, for instance:
|
||
#+begin_src sh
|
||
brew install multimarkdown
|
||
#+end_src
|
||
|
||
Also, I like Markdown is look like a word processor, similarly to my org files:
|
||
#+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"
|
||
markdown-header-scaling t)
|
||
:hook (markdown-mode . variable-pitch-mode)
|
||
:general
|
||
(:states 'normal :no-autoload t :keymaps 'markdown-mode-map
|
||
", l" '("insert link" . markdown-insert-link) ; Also C-c C-l
|
||
", i" '("insert image" . markdown-insert-image) ; Also C-c C-i
|
||
;; SPC u 3 , h for a third-level header:
|
||
", h" '("insert header" . markdown-insert-header-dwim)
|
||
", t" '(:ignore t :which-key "toggles")
|
||
", t t" '("toggle markup" . markdown-toggle-markup-hiding)
|
||
", t u" '("toggle urls" . markdown-toggle-markup-url-hiding)
|
||
", t i" '("toggle images" . markdown-toggle-markup-inline-images)
|
||
", t m" '("toggle math" . markdown-toggle-markup-math-hiding)
|
||
", d" '("do" . markdown-do)
|
||
", e" '("export" . markdown-export)
|
||
", p" '("preview" . markdown-preview)))
|
||
#+end_src
|
||
Note that the markdown-specific commands use the ~C-c C-c~ and ~C-c C-s~ prefixes.
|
||
|
||
With the =markdown-header-scaling= set, we no longer need to color the headers in Markdown.
|
||
#+begin_src emacs-lisp
|
||
(use-package markdown-mode
|
||
:config
|
||
(when window-system
|
||
(let ((default-color (face-attribute 'default :foreground)))
|
||
(set-face-attribute 'markdown-header-face nil
|
||
:font ha-variable-header-font
|
||
:foreground default-color))))
|
||
#+end_src
|
||
|
||
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
|
||
** ReStructured Text
|
||
Support for [[https://docutils.sourceforge.io/rst.html][reStructuredText]] is [[https://www.emacswiki.org/emacs/reStructuredText][well supported]] in Emacs.
|
||
#+begin_src emacs-lisp
|
||
(use-package rst
|
||
:hook (rst-mode . variable-pitch-mode)
|
||
:config
|
||
(set-face-attribute 'rst-literal nil :font ha-fixed-font))
|
||
#+end_src
|
||
** YAML
|
||
Doing 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 :tangle no
|
||
(when (treesit-available-p)
|
||
(use-package yaml-ts-mode
|
||
:mode ((rx ".y" (optional "a") "ml" string-end)
|
||
(rx (optional ".") "yamllint"))
|
||
:hook (yaml-ts-mode . display-line-numbers-mode)))
|
||
#+end_src
|
||
Get the latest version of =yaml-mode=:
|
||
#+begin_src emacs-lisp
|
||
(use-package yaml-mode
|
||
:mode (rx ".y" (optional "a") "ml" string-end)
|
||
(rx (optional ".") "yamllint")
|
||
:hook (yaml-mode . display-line-numbers-mode))
|
||
#+end_src
|
||
|
||
And we hook
|
||
#+begin_src emacs-lisp
|
||
(use-package yaml-pro
|
||
:straight (:host github :repo "zkry/yaml-pro")
|
||
:after yaml-mode
|
||
:hook (yaml-mode . yaml-pro-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 (treesit-available-p)
|
||
(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-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 :tangle no
|
||
(use-package ansible
|
||
:straight (:host github :repo "k1LoW/emacs-ansible")
|
||
:defer t
|
||
:mode ((rx (or "playbooks" "roles") (one-or-more any) ".y" (optional "a") "ml") . ansible-mode)
|
||
: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 :tangle no
|
||
(use-package ansible-doc
|
||
:hook (ansible-mode . ansible-doc-mode)
|
||
:after ansible
|
||
: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 install -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 :tangle 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
|