#+title: General Programming Configuration #+author: Howard X. Abrams #+date: 2020-10-26 #+tags: emacs programming yaml ansible docker json A literate programming file for helping me program. #+begin_src emacs-lisp :exports none ;;; general-programming --- Configuration for general languages. -*- lexical-binding: t; -*- ;; ;; © 2020-2023 Howard X. Abrams ;; Licensed under a Creative Commons Attribution 4.0 International License. ;; See http://creativecommons.org/licenses/by/4.0/ ;; ;; Author: Howard X. Abrams ;; 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/ha-programming.org ;; And tangle the file to recreate this one. ;; ;;; Code: #+end_src * Introduction Configuration for programming interfaces and workflows that behave similarly. * General The following work for all programming languages. ** Virtual Environments with direnv Farm off commands into /virtual environments/: #+begin_src emacs-lisp (use-package direnv :init (setq direnv-always-show-summary t direnv-show-paths-in-summary t) (if (file-exists-p "/opt/homebrew") (setq direnv--executable "/opt/homebrew/bin/direnv") (setq direnv--executable "/usr/local/bin/direnv")) :config (direnv-mode)) #+end_src ** Displaying Code *** Displaying Line Numbers For all programming languages, I would like to now default to absolute line numbers, and turn off the [[file:ha-display.org::*Mixed Pitch][Mixed Pitch]] feature that seems to /come along/ for the ride: #+begin_src emacs-lisp (use-package emacs :config (defun ha-prog-mode-config () "Configure the `prog-mode'" (setq display-line-numbers t) (mixed-pitch-mode 0)) :hook (prog-mode . ha-prog-mode-config)) #+end_src *** Code Folding While Emacs has options for viewing and moving around code, sometimes, we could /collapse/ all functions, and then start to expand them one at a time. For this, we could enable the built-in [[https://www.emacswiki.org/emacs/HideShow][hide-show feature]]: #+begin_src emacs-lisp :tangle no (use-package hide-show :straight (:type built-in) :init (setq hs-hide-comments t hs-hide-initial-comment-block t hs-isearch-open t) :hook (prog-mode . hs-minor-mode)) #+end_src Note that =hide-show= 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 ** Symbol Highlighting I appreciate calling =hi-lock-face-symbol-at-point= (or =highlight-symbol-at-point=) to highlight all instances of a variable or other symbol. #+begin_src emacs-lisp (use-package auto-highlight-symbol :config (setq ahs-idle-interval 0.1) (set-face-attribute ahs-face nil :foreground nil :background nil :weight 'ultra-bold :slant 'italic) (set-face-attribute ahs-plugin-default-face nil :foreground nil :background nil :weight 'bold :slant 'normal)) #+end_src Instead of calling =global-auto-highlight-symbol-mode=, we should just hook it to the =prog-mode=: #+begin_src emacs-lisp (use-package auto-highlight-symbol :hook ((prog-mode . auto-highlight-symbol-mode))) #+end_src Similarly, the [[https://github.com/wolray/symbol-overlay][symbol-overlay]] project highlights instances of symbols, but like =iedit= creates a keymap allowing manipulation of the symbols. The workflow is: 1. ~SPC t s s~ to highlight the symbol at point. 2. ~n~ and ~p~ to move from symbol to symbol. 3. Quitting the menu involves one of these: - ~q~ to leave point at spot - ~e~ to return to previous cursor placement - ~x~ to un-highlight all symbols 4. ~SPC t s s~ to highlight another symbol at point. 5. ~N~ and ~P~ to move to a /different symbol/. 6. ~r~ to rename symbol (like =iedit=) #+begin_src emacs-lisp (use-package symbol-overlay :config (pretty-hydra-define symbol-overlay (:color pink :quit-key "q") ("Show" (("s" symbol-overlay-put "highlight") ("t" symbol-overlay-toggle-in-scope "in scope")) "Navigate" (("n" symbol-overlay-jump-next "next") ; j? ("p" symbol-overlay-jump-prev "previous") ; k? ("<" symbol-overlay-jump-first "first") (">" symbol-overlay-jump-last "last")) "Switch" (("N" symbol-overlay-switch-forward "next") ("P" symbol-overlay-switch-backward "previous") ("d" symbol-overlay-jump-to-definition "definition")) "Edit" (("r" symbol-overlay-rename "replace" :color blue) ("R" symbol-overlay-query-replace "query replace" :color blue)) "Misc" (("w" symbol-overlay-save-symbol "to clipboard") ; y? ("C-s" symbol-overlay-isearch-literally "search all" :color blue)) "Exit" (("e" symbol-overlay-echo-mark "return" :color blue) ("x" symbol-overlay-remove-all "hide all" :color blue) ("q" nil "leave" :color blue)))) (ha-leader "t s" '("symbols" . symbol-overlay/body))) #+end_src While I created a Hydra for the commands, this project includes a keymap available only when the cursor (point) is on a highlighted symbol. These keybindings include: - ~n~ :: next matching symbol - ~p~ :: previous matching symbol - < :: jump first - > :: jump last - ~d~ :: jump to definition - ~e~ :: return to original point position - ~h~ :: help - ~i~ :: unhighlight symbol - ~q~ :: query replace - ~r~ :: rename - ~s~ :: isearch - ~t~ :: toggle in scope - ~w~ :: save symbol to clipboard After reading [[https://lmno.lol/alvaro/its-all-up-for-grabs-and-it-compounds][this essay]] by Álvaro Ramírez, I’ve been thinking of ways to connect services together. In my case, I am not sure I need [[https://github.com/magnars/multiple-cursors.el][multiple cursors]] (as symbol-overlay can rename the symbol which would be 90% of my use case), but I would like to highlight a symbol without actually moving to it. #+begin_src emacs-lisp (use-package symbol-overlay :after avy :config (defun avy-action-highlight-symbol (pt foobar) "Highlight symbol starting at PT at the current point." (save-excursion (avy-action-goto pt foobar) (symbol-overlay-put)) t) (add-to-list 'avy-dispatch-alist '(?S . avy-action-highlight-symbol))) #+end_src ** Spell Checking Comments The [[https://www.emacswiki.org/emacs/FlySpell#h5o-2][flyspell-prog-mode]] checks for misspellings in comments. #+begin_src emacs-lisp (use-package flyspell :hook (prog-mode . flyspell-prog-mode)) #+end_src ** Linting with Flycheck Why use [[https://www.flycheck.org/][flycheck]] over the built-in =flymake=? Speed used to be the advantage, but 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 :straight (:host github :repo "flycheck/flycheck") :init (setq next-error-message-highlight t) :bind (:map flycheck-error-list-mode-map ("C-n" . 'flycheck-error-list-next-error) ("C-p" . 'flycheck-error-list-previous-error) ("j" . 'flycheck-error-list-next-error) ("k" . 'flycheck-error-list-previous-error)) :config (defun flycheck-enable-checker () "Not sure why flycheck disables working checkers." (interactive) (let (( current-prefix-arg '(4))) ; C-u (call-interactively 'flycheck-disable-checker))) (flymake-mode -1) (global-flycheck-mode) (ha-leader "t c" 'flycheck-mode) (ha-leader ">" '("next problem" . flycheck-next-error) "<" '("previous problem" . flycheck-previous-error) "e" '(:ignore t :which-key "errors") "e n" '(flycheck-next-error :repeat t :wk "next") "e N" '(flycheck-next-error :repeat t :wk "next") "e p" '(flycheck-previous-error :repeat t :wk "previous") "e P" '(flycheck-previous-error :repeat t :wk "previous") "e b" '("error buffer" . flycheck-buffer) "e c" '("clear" . flycheck-clear) "e l" '("list all" . flycheck-list-errors) "e g" '("goto error" . counsel-flycheck) "e y" '("copy errors" . flycheck-copy-errors-as-kill) "e s" '("select checker" . flycheck-select-checker) "e ?" '("describe checker" . flycheck-describe-checker) "e h" '("display error" . flycheck-display-error-at-point) "e e" '("explain error" . flycheck-explain-error-at-point) "e H" '("help" . flycheck-info) "e i" '("manual" . flycheck-manual) "e V" '("verify-setup" . flycheck-verify-setup) "e v" '("version" . flycheck-verify-checker) "e E" '("enable checker" . flycheck-enable-checker) "e x" '("disable checker" . flycheck-disable-checker) "e t" '("toggle flycheck" . flycheck-mode))) #+end_src ** Language Documentation Used to use the Dash project for searching documentation associated with a programming language, but that hardly worked on my Linux systems. I’m interested in using [[https://devdocs.io/][devdocs.io]] instead, which is similar, but displays it in simple HTML. This can keep it all /inside/ Emacs. Two Emacs projects compete for this position. The Emacs [[https://github.com/astoff/devdocs.el][devdocs]] project is active, and seems to work well. Its advantage is a special mode for moving around the documentation. #+begin_src emacs-lisp (use-package devdocs :general (:states 'normal "gD" '("devdocs" . ha-devdocs-major-mode)) :config (pretty-hydra-define hydra-devdocs (:color blue) ("Dev Docs" (("d" ha-devdocs-major-mode "open") ("p" devdocs-peruse "peruse")) "Packages" (("i" devdocs-install "install") ("u" devdocs-update-all "update") ("x" devdocs-delete "uninstall"))))) #+end_src The =devdocs-lookup= command attempts to guess which documentation it should display based on the mode, but if I’m editing YAML files, I actually want to pull up the Ansible documentation, and probably the Jinja ones too. #+begin_src emacs-lisp :tangle no (defun ha-devdocs-major-mode () "My mapping of major mode to Devdocs slug." (interactive) (let ((devdocs-current-docs (cl-case major-mode ('emacs-lisp-mode '("elisp")) ('python-mode '("python~.3.11")) ('yaml-ts-mode '("ansible" "jinja-2.11"))))) (devdocs-lookup nil))) #+end_src ** Navigation *** Move by Functions The =mark-paragraph= and =downcase-word= 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 ; (global-set-key (kbd "M-k") 'beginning-of-defun) ; (global-set-key (kbd "M-j") 'beginning-of-next-defun) (when (fboundp 'evil-define-key) (evil-define-key '(normal insert emacs) prog-mode-map (kbd "M-k") 'beginning-of-defun (kbd "M-j") 'beginning-of-next-defun)) #+end_src But one of those functions 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 <= **** 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 (when (treesit-available-p) (use-package treesit :straight (:type built-in) :preface (setq treesit-language-source-alist '((bash "https://github.com/tree-sitter/tree-sitter-bash") ;; (c "https://github.com/tree-sitter/tree-sitter-c/" "master" "src") (clojure "https://github.com/sogaiu/tree-sitter-clojure" "master" "src") ;; (cpp "https://github.com/tree-sitter/tree-sitter-cpp/" "master" "src") ;; (cmake "https://github.com/uyha/tree-sitter-cmake") (css "https://github.com/tree-sitter/tree-sitter-css") (dockerfile "https://github.com/camdencheek/tree-sitter-dockerfile" "main" "src") ;; From my private cloned repository: ;; (dockerfile "file:///opt/src/github/tree-sitter-dockerfile" "main" "src") ;; The Emacs Lisp Tree Sitter doesn't work with Emacs (go figure): ;; (elisp "https://github.com/Wilfred/tree-sitter-elisp") ;; (elixir "https://github.com/elixir-lang/tree-sitter-elixir" "main" "src") ;; (erlang "https://github.com/WhatsApp/tree-sitter-erlang" "main" "src") (go "https://github.com/tree-sitter/tree-sitter-go") ;; (haskell "https://github.com/tree-sitter/tree-sitter-haskell" "master" "src") (html "https://github.com/tree-sitter/tree-sitter-html") ;; (java "https://github.com/tree-sitter/tree-sitter-java" "master" "src") ;; (javascript "https://github.com/tree-sitter/tree-sitter-javascript" "master" "src") (json "https://github.com/tree-sitter/tree-sitter-json") ;; (julia "https://github.com/tree-sitter/tree-sitter-julia" "master" "src") ;; (lua "https://github.com/MunifTanjim/tree-sitter-lua" "main" "src") (make "https://github.com/alemuller/tree-sitter-make") (markdown "https://github.com/ikatyang/tree-sitter-markdown") ;; (meson "https://github.com/Decodetalkers/tree-sitter-meson" "master" "src") (python "https://github.com/tree-sitter/tree-sitter-python") (ruby "https://github.com/tree-sitter/tree-sitter-ruby" "master" "src") (rust "https://github.com/tree-sitter/tree-sitter-rust" "master" "src") (toml "https://github.com/tree-sitter/tree-sitter-toml") ;; (tsx "https://github.com/tree-sitter/tree-sitter-typescript" "master" "tsx/src") ;; (typescript "https://github.com/tree-sitter/tree-sitter-typescript" "master" "typescript/src") (yaml "https://github.com/ikatyang/tree-sitter-yaml"))) (defun mp-setup-install-grammars () "Install Tree-sitter grammars if they are absent." (interactive) (sit-for 30) (mapc #'treesit-install-language-grammar (mapcar #'car treesit-language-source-alist))) ;; Optional, but Mickey recommends. Tree-sitter enabled major ;; modes are distinct from their ordinary counterparts, however, ;; the `tree-sitter-mode' can't be enabled if we use this ;; feature. ;; ;; You can remap major modes with `major-mode-remap-alist'. Note ;; this does *not* extend to hooks! Make sure you migrate them also ;; (dolist (mapping '((bash-mode . bash-ts-mode) ;; (sh-mode . bash-ts-mode) ;; (css-mode . css-ts-mode) ;; (dockerfile-mode . dockerfile-ts-mode) ;; (json-mode . json-ts-mode) ;; (makefile-mode . makefile-ts-mode) ;; (python-mode . python-ts-mode) ;; (ruby-mode . ruby-ts-mode) ;; (yaml-mode . yaml-ts-mode))) ;; (add-to-list 'major-mode-remap-alist mapping)) ;; Can we (do we need to) update this list? ;; (add-to-list 'tree-sitter-major-mode-language-alist mapping)) :config (mp-setup-install-grammars))) #+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 (when (treesit-available-p) (use-package combobulate :straight (:host github :repo "mickeynp/combobulate") :after treesit :hook ((yaml-ts-mode . combobulate-mode) ;; (css-ts-mode . combobulate-mode) ;; (json-ts-mode . combobulate-mode) ;; (python-ts-mode . combobulate-mode) ) )) #+end_src Now, I can create an /interface/ of keystrokes to jump around like a boss: #+begin_src emacs-lisp (when (treesit-available-p) (use-package combobulate :general (:states 'visual :keymaps 'combobulate-key-map "o" '("mark node" . combobulate-mark-node-dwim)) ; Mark symbol since "o" doesn't do anything (:states 'normal :keymaps 'combobulate-key-map "g J" '("avy jump" . combobulate-avy) "[ [" '("prev node" . combobulate-navigate-logical-previous) "] ]" '("next node" . combobulate-navigate-logical-next) "[ f" '("prev defun" . combobulate-navigate-beginning-of-defun) "] f" '("next defun" . combobulate-navigate-end-of-defun) "[ m" '("drag back" . combobulate-drag-up) "] m" '("drag forward" . combobulate-drag-down) "[ r" '("raise" . combobulate-vanish-node) "g j" '(:ignore t :which-key "combobulate jump") "g j j" '("all" . combobulate-avy-jump) "g j s" '("strings" . ha-combobulate-string) "g j c" '("comments" . ha-combobulate-comment) "g j i" '("conditionals" . ha-combobulate-conditional) "g j l" '("loops" . ha-combobulate-loop) "g j f" '("functions" . combobulate-avy-jump-defun)) :pretty-hydra ((:color pink :quit-key "q") ("Navigation" (("j" combobulate-navigate-logical-next "Next") ("k" combobulate-navigate-logical-previous "Previous") ("h" combobulate-navigate-beginning-of-defun "Defun <") ("l" combobulate-navigate-end-of-defun "Defun >") ("g" combobulate-avy-jump "Avy Jump")) "Push" (("U" combobulate-drag-up "Drag back") ("D" combobulate-drag-down "Drag forward") ("R" combobulate-vanish-node "Drag back")) "Jump" (("s" ha-combobulate-string "to string" :color blue) ("c" ha-combobulate-comment "comments" :color blue) ("i" ha-combobulate-conditional "conditionals" :color blue) ("l" ha-combobulate-loop "loops" :color blue) ("f" combobulate-avy-jump-defun "to defuns" :color blue)))))) #+end_src Mickey’s interface is the [[help:combobulate][combobulate]] function (or ~C-c o o~), but mine is more /evil/. I can create a /helper function/ to allow me to jump to various types of—well, /types/: #+begin_src emacs-lisp (when (treesit-available-p) (use-package combobulate :config (defun ha-combobulate-string () "Call `combobulate-avy-jump' searching for strings." (interactive) (with-navigation-nodes (:nodes '("string")) (combobulate-avy-jump)))) (defun ha-combobulate-comment () "Call `combobulate-avy-jump' searching for comments." (interactive) (with-navigation-nodes (:nodes '("comment")) (combobulate-avy-jump))) (defun ha-combobulate-conditional () "Call `combobulate-avy-jump' searching for conditionals." (interactive) (with-navigation-nodes (:nodes '("conditional_expression" "if_statement" "if_clause" "else_clause" "elif_clause")) (combobulate-avy-jump))) (defun ha-combobulate-loop () "Call `combobulate-avy-jump' searching for loops." (interactive) (with-navigation-nodes (:nodes '("for_statement" "for_in_clause" "while_statement" "list_comprehension" "dictionary_comprehension" "set_comprehension")) (combobulate-avy-jump)))) #+end_src *** Evil Text Object from Tree Sitter With Emacs version 29, we get a better approach to parsing languages, and this means that our [[https://github.com/nvim-treesitter/nvim-treesitter-textobjects#built-in-textobjects][text objects]] can be better too with the [[https://github.com/meain/evil-textobj-tree-sitter][evil-textobj-tree-sitter project]]: #+begin_src emacs-lisp :tangle no (when (and (treesit-available-p) (fboundp 'evil-define-text-object)) (use-package evil-textobj-tree-sitter :config ;; We need to bind keys to the text objects found at: ;; https://github.com/nvim-treesitter/nvim-treesitter-textobjects#built-in-textobjects ;; bind `function.outer`(entire function block) to `f` for use in things like `vaf`, `yaf` (define-key evil-outer-text-objects-map "f" (evil-textobj-tree-sitter-get-textobj "function.outer")) ;; bind `function.inner`(function block without name and args) to `f` for use in things like `vif`, `yif` (define-key evil-inner-text-objects-map "f" (evil-textobj-tree-sitter-get-textobj "function.inner")) (define-key evil-outer-text-objects-map "c" (evil-textobj-tree-sitter-get-textobj "comment.outer")) (define-key evil-inner-text-objects-map "c" (evil-textobj-tree-sitter-get-textobj "comment.inner")) (define-key evil-outer-text-objects-map "u" (evil-textobj-tree-sitter-get-textobj "conditional.outer")) (define-key evil-inner-text-objects-map "u" (evil-textobj-tree-sitter-get-textobj "conditional.inner")) (define-key evil-outer-text-objects-map "b" (evil-textobj-tree-sitter-get-textobj "loop.outer")) (define-key evil-inner-text-objects-map "b" (evil-textobj-tree-sitter-get-textobj "loop.inner")))) #+end_src Seems the macro, =evil-textobj-tree-sitter-get-textobj= has a bug, so the following—which would have been easier to write—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) ;; Never using the etags backend. GNU Global? Maybe. (remove-hook 'xref-backend-functions #'etags--xref-backend)) #+end_src While I’m at it, let’s connect various ~g~ sequence keys to =xref-= interface functions: #+begin_src emacs-lisp (use-package emacs :general (:states 'normal "g ." '("find def" . xref-find-definitions) "g >" '("find def o/win" . xref-find-definitions-other-window) "g ," '("def go back" . xref-go-back) "g <" '("def go forward" . xref-go-forward) "g /" '("find refs" . xref-find-references) "g ?" '("find/rep refs" . xref-find-references-and-replace) "g h" '("find apropos" . xref-find-apropos) "g b" '("def go back" . xref-go-back))) #+end_src I have two different /jumping/ systems, the [[info:emacs#Xref][Xref interface]] and 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. 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 **** 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 *** 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. *** LSP 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 ** General Code Editing *** iEdit While there are language-specific ways to rename variables and functions, [[https://github.com/victorhge/iedit][iedit]] is often sufficient. #+begin_src emacs-lisp :tangle no (use-package iedit :config (ha-leader "s e" '("iedit" . iedit-mode))) #+end_src While =iedit= acts a little odd with Evil, the [[https://github.com/syl20bnr/evil-iedit-state][evil-iedit-state project]] attempts to makes the interface more intuitive. This creates both an =iedit= and =iedit-insert= states. Calling ~Escape~ from =iedit-insert= goes to =iedit=, and hitting it again, will go back to =normal= state. To use, highlight a region with ~v~, and continue to hit ~v~ until you’ve selected the variable/symbol, and then type ~e~. Or, highlight normally, e.g. ~v i o~, and hit ~E~: #+begin_src emacs-lisp (when (fboundp 'evil-mode) (use-package evil-iedit-state :after iedit :general (:states 'visual "E" '("iedit" . evil-iedit-state/iedit-mode)))) #+end_src The =iedit-insert= state is pretty much /regular/ =insert= state, so the interesting keys are in =iedit= state: - ~0~ / ~$~ :: jump to beginning/end of the “occurrence” - ~n~ / ~N~ :: jump to next / previous occurrence - ~I~ / ~A~ :: jump to beginning/end of occurrence and go into =iedit-insert= mode (obviously ~a~ and ~i~ do too) - ~#~ :: highlights all the matching occurrences - ~F~ :: restricts to the current function *** Case Conversion The [[https://github.com/akicho8/string-inflection][string-inflection]] project (see [[http://sodaware.sdf.org/notes/converting-to-snake-case-in-emacs/][this overview]]) converts symbol variables to /appropriate format/ for the mode. This replaces my home-brewed functions. #+begin_src emacs-lisp (use-package string-inflection :general (:states '(normal visual motion operator) "z s" '("to snake case" . string-inflection-underscore) "z S" '("to Snake Case" . string-inflection-upcase) "z c" '("to camelCase" . string-inflection-lower-camelcase) "z C" '("to CamelCase" . string-inflection-camelcase) "z -" '("to kebab case" . string-inflection-kebab-case) "z z" '("toggle snake/camel" . string-inflection-all-cycle))) #+end_src I would like to have this bound on the ~g~ sequence, but that is crowded. Note that ~g u~ (for lower-casing stuff), and ~g U~ (for up-casing) requires /something/, for instance ~g U i o~ upper-cases the symbol at point. These functions, however, only work with a symbol (which is the typical case). ** Inline Code Evaluation While I like [[help:eval-print-last-sexp][eval-print-last-sexp]], I would like a bit of formatting in order to /keep the results/ in the file. #+begin_src emacs-lisp (defun ha-eval-print-last-sexp (&optional internal-arg) "Evaluate the expression located before the point. Insert results back into the buffer at the end of the line after a comment." (interactive) (save-excursion (eval-print-last-sexp internal-arg)) (end-of-line) (insert " ") (insert comment-start) (insert "⟹ ") (dotimes (i 2) (next-line) (join-line))) #+end_src Typical keybindings for all programming modes: #+begin_src emacs-lisp (ha-local-leader :keymaps 'prog-mode-map "e" '(:ignore t :which-key "eval") "e ;" '("expression" . eval-expression) "e b" '("buffer" . eval-buffer) "e f" '("function" . eval-defun) "e r" '("region" . eval-region) "e e" '("eval exp" . eval-last-sexp) "e p" '("print s-exp" . ha-eval-print-last-sexp)) #+end_src ** Ligatures The idea of using math symbols for a programming languages keywords is /cute/, but can be confusing, so I use it sparingly: #+begin_src emacs-lisp (defun ha-prettify-prog () "Extends the `prettify-symbols-alist' for programming." (mapc (lambda (pair) (push pair prettify-symbols-alist)) '(("lambda" . "𝝀") (">=" . "≥") ("<=" . "≤") ("!=" . "≠"))) (prettify-symbols-mode)) (add-hook 'prog-mode-hook 'ha-prettify-prog) #+end_src Hopefully I can follow [[https://www.masteringemacs.org/article/unicode-ligatures-color-emoji][Mickey Petersen's essay]] on getting full ligatures working, but right now, they 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 :tangle no (use-package unicode-fonts :config (ignore-errors (unicode-fonts-setup))) #+end_src ** Compiling The [[help:compile][compile]] function lets me enter a command to run, or I can search the history for a previous run. What it 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 (project-root (project-current)))) ;; Make a list of ALL the things. ;; Note that `concat' returns an empty string if you give it null, ;; so we use `-concat' the dash library: (-concat compile-history (ha--makefile-completions) (ha--toxfile-completions) (when (and (boundp 'compile-command-list) (listp compile-command-list)) compile-command-list)))) #+end_src My replacement to [[help:compile][compile]] uses my new =completing-read= function: #+begin_src emacs-lisp (defun ha-project-compile (command) "Run `compile' from a list of directory-specific commands." (interactive (list (completing-read "Compile command: " (ha--compile-command-list) nil nil "" 'compile-history))) (let ((default-directory (project-root (project-current)))) (cond ((string-match rx-compile-to-vterm command) (ha-compile-vterm command)) ((string-match rx-compile-to-eshell command) (ha-compile-eshell command)) (t (compile command))))) #+end_src If I end a command with a =|v=, it sends the compile command to a vterm session for the project, allowing me to continue the commands: #+begin_src emacs-lisp (defvar rx-compile-to-vterm (rx "|" (0+ space) "v" (0+ space) line-end)) (defun ha-compile-vterm (full-command &optional project-dir) (unless project-dir (setq project-dir (project-name (project-current)))) ;; (add-to-list 'compile-history full-command) (let ((command (replace-regexp-in-string rx-compile-to-vterm "" full-command))) (ha-ssh-send command project-dir))) #+end_src And what about sending the command to Eshell as well? #+begin_src emacs-lisp (defvar rx-compile-to-eshell (rx "|" (0+ space) "s" (0+ space) line-end)) (defun ha-compile-eshell (full-command &optional project-dir) "Send a command to the currently running Eshell terminal. If a terminal isn't running, it will be started, allowing follow-up commands." (unless project-dir (setq project-dir (project-name (project-current)))) (let ((command (replace-regexp-in-string rx-compile-to-eshell "" full-command))) (ha-eshell-send command project-dir))) #+end_src And 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 "<>")))) #+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)) ;; Jump forward using the evil-jump-item ... change this to one ;; of the functions in thing-at-point? (when (fboundp 'evil-jump-item) (evil-jump-item)) (setq e (1+ (point)))) ;; (narrow-to-region s e) (shell-command-on-region s e (concat "jq " query) nil t "*jq errors*")))) (ha-local-leader :keymaps '(js-json-mode-map json-ts-mode-map) "j" 'ha-json-buffer-to-jq) #+end_src This means, that some data like: #+begin_src json :tangle no { "common_id": "GMC|F2BADC23|64D52BF7|awardlateengine", "data": { "name": "Create And Wait for Service Image", "description": "Creates a new Service Image using IMaaS", "long_description": "This job creates a new 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 :mode ((rx ".md" string-end) . gfm-mode) :init (setq markdown-command (expand-file-name "markdown" "~/bin") markdown-open-command (expand-file-name "markdown-open" "~/bin") markdown-header-scaling t) :general (:states 'normal :no-autoload t :keymaps 'markdown-mode-map ", l" '("insert link" . markdown-insert-link) ; Also C-c C-l ", i" '("insert image" . markdown-insert-image) ; Also C-c C-i ;; SPC u 3 , h for a third-level header: ", h" '("insert header" . markdown-insert-header-dwim) ", t" '(:ignore t :which-key "toggles") ", t t" '("toggle markup" . markdown-toggle-markup-hiding) ", t u" '("toggle urls" . markdown-toggle-markup-url-hiding) ", t i" '("toggle images" . markdown-toggle-markup-inline-images) ", t m" '("toggle math" . markdown-toggle-markup-math-hiding) ", d" '("do" . markdown-do) ", e" '("export" . markdown-export) ", p" '("preview" . markdown-preview))) #+end_src Note that the markdown-specific commands use the ~C-c C-c~ and ~C-c C-s~ prefixes. Let’s make sure that [[https://www.flycheck.org/en/latest/languages.html#markdown][markdown]] is proper using [[https://pypi.org/project/pymarkdownlnt/][PyMarkdown]]. First, get the script installed globally: #+begin_src sh pip install pymarkdown #+end_src And then we can use it. For some reason, the =pymarkdown= (which I need to use from work) doesn’t seem to be part of the version of Flycheck available on Melpa, so… #+begin_src emacs-lisp (use-package markdown-mode :after flycheck :config (setq flycheck-markdown-pymarkdown-config ".pymarkdown.yml") (flycheck-may-enable-checker 'markdown-pymarkdown)) ;; defcustom flycheck-markdown-pymarkdown-config #+end_src Both the =markdown-command= and the =markdown-open-command= variables are called to render (and preview) a Markdown file (~C-c C-c o~), and calls the following scripts (which in turn, call =pandoc= as I depend on this for other org-related features): #+begin_src sh :tangle ~/bin/markdown :shebang "#!/usr/bin/env bash" :tangle-mode u+x pandoc --to=html --from=gfm $* #+end_src #+begin_src sh :tangle ~/bin/markdown-open :shebang "#!/usr/bin/env bash" :tangle-mode u+x OUTPUT_FILE=$(mktemp 'emacs-view-XXXXXXX.html') pandoc --to=html --from=gfm --output=$OUTPUT_FILE $* # Are we on a MacOS Laptop: if [ -d "/Library" ] then open $OUTPUT_FILE else firefox -new-tab $OUTPUT_FILE fi #+end_src 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 :config (set-face-attribute 'rst-literal nil :font ha-fixed-font)) #+end_src ** Docker Edit =Dockerfiles= with the [[https://github.com/spotify/dockerfile-mode][dockerfile-mode]] project: #+BEGIN_SRC emacs-lisp (use-package dockerfile-mode :mode (rx string-start "Dockerfile") :config (make-local-variable 'docker-image-name) (defvaralias 'docker-image-name 'dockerfile-image-name nil) (ha-local-leader :keymaps 'dockerfile-mode-map "b" '("build" . dockerfile-build-buffer) "B" '("build no cache" . dockerfile-build-no-cache-buffer) "t" '("insert build tag" . ha-dockerfile-build-insert-header)) (defun ha-dockerfile-build-insert-header (image-name) "Prepends the default Dockerfile image name at the top of a file." (interactive "sDefault image name: ") (save-excursion (goto-char (point-min)) (insert (format "## -*- dockerfile-image-name: \"%s\" -*-" image-name)) (newline)))) #+END_SRC /Control/ Docker from Emacs using the [[https://github.com/Silex/docker.el][docker.el]] project: #+BEGIN_SRC emacs-lisp (use-package docker :commands docker :config (ha-leader "a d" 'docker)) #+END_SRC Unclear whether I want to Tramp into a running container: #+BEGIN_SRC emacs-lisp :tangle no (use-package docker-tramp :defer t :after docker) #+END_SRC ** Shell Scripts While I don't like writing them, I can't get away from them. Check out the goodies in [[https://www.youtube.com/watch?v=LTC6SP7R1hA&t=5s][this video]]. While filename extensions work fine most of the time, I don't like to pre-pend =.sh= to the shell scripts I write, and instead, would like to associate =shell-mode= with all files in a =bin= directory: #+begin_src emacs-lisp (use-package sh-mode :straight (:type built-in) :mode (rx (or (seq ".sh" eol) "/bin/")) :init (setq sh-basic-offset 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:t todo:nil tasks:nil tags:nil date:nil #+options: skip:nil author:nil email:nil creator:nil timestamp:nil #+infojs_opt: view:nil toc:t ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js