The lispy+lispyville is more feature-rich than the smart-parens+clever. Both are fantastic, but I'm switching my lisp work for lispyville.
		
			
				
	
	
		
			727 lines
		
	
	
		
			32 KiB
		
	
	
	
		
			Org Mode
		
	
	
	
	
	
			
		
		
	
	
			727 lines
		
	
	
		
			32 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
 | ||
| Seems that all programming interfaces and workflows behave similarly. One other helper routine is a =general= macro for org-mode files:
 | ||
| #+begin_src emacs-lisp
 | ||
|   (general-create-definer ha-prog-leader
 | ||
|       :states '(normal visual motion)
 | ||
|       :keymaps 'prog-mode-map
 | ||
|       :prefix ","
 | ||
|       :global-prefix "<f17>"
 | ||
|       :non-normal-prefix "S-SPC")
 | ||
| #+end_src
 | ||
| * General
 | ||
| The following work for all programming languages.
 | ||
| ** direnv
 | ||
| Farm off commands into /virtual environments/:
 | ||
| #+begin_src emacs-lisp
 | ||
|   (use-package direnv
 | ||
|     :init
 | ||
|     (setq direnv--executable "/usr/local/bin/direnv"
 | ||
|           direnv-always-show-summary t
 | ||
|           direnv-show-paths-in-summary t)
 | ||
|     :config
 | ||
|     (direnv-mode))
 | ||
| #+end_src
 | ||
| ** Spell Checking Comments
 | ||
| The [[https://www.emacswiki.org/emacs/FlySpell#h5o-2][flyspell-prog-mode]] checks for misspellings in comments.
 | ||
| 
 | ||
| #+begin_src emacs-lisp
 | ||
|   (use-package flyspell
 | ||
|     :hook (prog-mode . flyspell-prog-mode))
 | ||
| #+end_src
 | ||
| ** Flycheck
 | ||
| Why use [[https://www.flycheck.org/][flycheck]] over the built-in =flymake=? Speed used to be the advantage, but I’m now pushing much of this to LSP, so speed is less of an issue.  What about when I am not using LSP? Also, since I’ve hooked grammar checkers, I need this with global keybindings.
 | ||
| 
 | ||
| #+begin_src emacs-lisp
 | ||
|   (use-package flycheck
 | ||
|     :init
 | ||
|     (setq next-error-message-highlight t)
 | ||
|     :bind (:map flycheck-error-list-mode-map
 | ||
|                 ("C-n" . 'flycheck-error-list-next-error)
 | ||
|                 ("C-p" . 'flycheck-error-list-previous-error)
 | ||
|                 ("j"   . 'flycheck-error-list-next-error)
 | ||
|                 ("k"   . 'flycheck-error-list-previous-error))
 | ||
|     :config
 | ||
|     (defun flycheck-enable-checker ()
 | ||
|       "Not sure why flycheck disables working checkers."
 | ||
|       (interactive)
 | ||
|       (let (( current-prefix-arg '(4))) ; C-u
 | ||
|         (call-interactively 'flycheck-disable-checker)))
 | ||
| 
 | ||
|     (flymake-mode -1)
 | ||
|     (global-flycheck-mode)
 | ||
|     (ha-leader "t c" 'flycheck-mode)
 | ||
| 
 | ||
|     (ha-leader
 | ||
|       ">" '("next problem" . flycheck-next-error)
 | ||
|       "<" '("previous problem" . flycheck-previous-error)
 | ||
| 
 | ||
|       "e" '(:ignore t :which-key "errors")
 | ||
|       "e n" '(flycheck-next-error     :repeat t :wk "next")
 | ||
|       "e N" '(flycheck-next-error     :repeat t :wk "next")
 | ||
|       "e p" '(flycheck-previous-error :repeat t :wk "previous")
 | ||
|       "e P" '(flycheck-previous-error :repeat t :wk "previous")
 | ||
| 
 | ||
|       "e b" '("error buffer"     . flycheck-buffer)
 | ||
|       "e c" '("clear"            . flycheck-clear)
 | ||
|       "e l" '("list all"         . flycheck-list-errors)
 | ||
|       "e g" '("goto error"       . counsel-flycheck)
 | ||
|       "e y" '("copy errors"      . flycheck-copy-errors-as-kill)
 | ||
|       "e s" '("select checker"   . flycheck-select-checker)
 | ||
|       "e ?" '("describe checker" . flycheck-describe-checker)
 | ||
|       "e h" '("display error"    . flycheck-display-error-at-point)
 | ||
|       "e e" '("explain error"    . flycheck-explain-error-at-point)
 | ||
|       "e H" '("help"             . flycheck-info)
 | ||
|       "e i" '("manual"           . flycheck-manual)
 | ||
|       "e V" '("verify-setup"     . flycheck-verify-setup)
 | ||
|       "e v" '("version"          . flycheck-verify-checker)
 | ||
|       "e E" '("enable checker"   . flycheck-enable-checker)
 | ||
|       "e x" '("disable checker"  . flycheck-disable-checker)
 | ||
|       "e t" '("toggle flycheck"  . flycheck-mode)))
 | ||
| #+end_src
 | ||
| ** Documentation
 | ||
| I’m interested in using [[https://devdocs.io/][devdocs]] instead, which is similar, but keeps it all /inside/ Emacs (and works on my Linux system). Two Emacs projects compete for this position. The Emacs [[https://github.com/astoff/devdocs.el][devdocs]] project is active, and seems to work well. Its advantage is a special mode for moving around the documentation.
 | ||
| #+begin_src emacs-lisp
 | ||
|   (use-package devdocs
 | ||
|     :general (:states 'normal "gD" 'devdocs-lookup)
 | ||
| 
 | ||
|     :config
 | ||
|     (ha-prog-leader
 | ||
|       "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-prog-leader
 | ||
|       "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.
 | ||
| ** Navigation with dumb-jump
 | ||
| ** 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
 | ||
| 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
 | ||
|     :init
 | ||
|     (setq dumb-jump-prefer-searcher 'rg)
 | ||
| 
 | ||
|     :config
 | ||
|     (setq xref-show-definitions-function #'xref-show-definitions-completing-read)
 | ||
| 
 | ||
|     (add-hook 'xref-backend-functions #'dumb-jump-xref-activate)
 | ||
|     ;; (add-to-list 'evil-goto-definition-functions #'dumb-jump)
 | ||
| 
 | ||
|     ;; 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-prog-leader
 | ||
|       "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." 'xref-find-definitions
 | ||
|                       "g>" 'xref-find-definitions-other-window
 | ||
|                       "g," 'xref-go-back
 | ||
|                       "g<" 'xref-go-forward
 | ||
|                       "g/" 'xref-find-references
 | ||
|                       "g?" 'xref-find-references-and-replace
 | ||
|                       "gh" 'xref-find-apropos
 | ||
|                       "gb" '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-prog-leader
 | ||
|       "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
 | ||
|              "SPC m w r" '("restart"  . lsp-reconnect)
 | ||
|              "SPC m w b" '("events"   . lsp-events-buffer)
 | ||
|              "SPC m w e" '("errors"   . lsp-stderr-buffer)
 | ||
|              "SPC m w q" '("quit"     . lsp-shutdown)
 | ||
|              "SPC m w Q" '("quit all" . lsp-shutdown-all)
 | ||
| 
 | ||
|              "SPC m l r" '("rename"   . lsp-rename)
 | ||
|              "SPC m l f" '("format"   . lsp-format)
 | ||
|              "SPC m l a" '("actions"  . lsp-code-actions)
 | ||
|              "SPC m l i" '("imports"  . lsp-code-action-organize-imports)
 | ||
|              "SPC m 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 =SPC m= 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-prog-leader
 | ||
|         "g"  '(:ignore t :which-key "goto")
 | ||
|         "g m" '("imenu" . lsp-ui-imenu))
 | ||
|       (add-hook 'lsp-after-open-hook 'lsp-enable-imenu))
 | ||
| #+end_src
 | ||
| *** Display Configuration
 | ||
| Using the [[https://github.com/seagle0128/doom-modeline][Doom Modeline]] to add notifications:
 | ||
| #+begin_src emacs-lisp
 | ||
|   (use-package doom-modeline
 | ||
|     :config
 | ||
|     (setq doom-modeline-lsp t
 | ||
|           doom-modeline-env-version t))
 | ||
| #+end_src
 | ||
| ** Function Call Notifications
 | ||
| As I've mentioned [[http://www.howardism.org/Technical/Emacs/beep-for-emacs.html][on my website]], I've created a [[file:~/website/Technical/Emacs/beep-for-emacs.org][beep function]] that notifies when long running processes complete.
 | ||
| 
 | ||
| #+begin_src emacs-lisp
 | ||
|   (use-package alert
 | ||
|     :init
 | ||
|     (setq alert-default-style
 | ||
|           (if (ha-running-on-macos?)
 | ||
|               'osx-notifier
 | ||
|             'libnotify)))
 | ||
| 
 | ||
|   (use-package beep
 | ||
|     :straight nil   ; Already in the load-path
 | ||
|     :hook (after-init . (lambda () (beep--when-finished "Emacs has started." "Eemacs has started")))
 | ||
|     :config
 | ||
|     (dolist (func '(org-publish
 | ||
|                     org-publish-all
 | ||
|                     org-publish-project
 | ||
|                     compile
 | ||
|                     shell-command))
 | ||
|       (advice-add func :around #'beep-when-runs-too-long)))
 | ||
| #+end_src
 | ||
| While that code /advices/ the publishing and compile commands, I may want to add more.
 | ||
| ** iEdit
 | ||
| While there are language-specific ways to rename variables and functions, [[https://github.com/victorhge/iedit][iedit]] is often sufficient.
 | ||
| 
 | ||
| #+begin_src emacs-lisp
 | ||
|   (use-package iedit
 | ||
|     :config
 | ||
|     (ha-leader "s e" '("iedit" . iedit-mode)))
 | ||
| #+end_src
 | ||
| ** Commenting
 | ||
| I like =comment-dwim= (~M-;~), and I like =comment-box=, but I have an odd personal style that I like to codify:
 | ||
| 
 | ||
| #+begin_src emacs-lisp
 | ||
|   (defun ha-comment-line (&optional start end)
 | ||
|     "Comment a line or region with a block-level format.
 | ||
|   Calls `comment-region' with START and END set to the region or
 | ||
|   the start and end of the line."
 | ||
|     (interactive)
 | ||
|     (when (or (null start) (not (region-active-p)))
 | ||
|       (setq start (line-beginning-position))
 | ||
|       (setq end   (line-end-position)))
 | ||
|     (save-excursion
 | ||
|       (narrow-to-region start end)
 | ||
|       (upcase-region start end)
 | ||
|       (goto-char (point-min))
 | ||
|       (insert "------------------------------------------------------------------------\n")
 | ||
|       (goto-char (point-max))
 | ||
|       (insert "\n------------------------------------------------------------------------")
 | ||
|       (comment-region (point-min) (point-max))
 | ||
|       (widen)))
 | ||
| #+end_src
 | ||
| And a keybinding:
 | ||
| #+begin_src emacs-lisp
 | ||
|   (ha-prog-leader "c" '("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-prog-leader
 | ||
|      "e"  '(:ignore t :which-key "eval")
 | ||
|      "e ;" '("expression" . eval-expression)
 | ||
|      "e b" '("buffer" . eval-buffer)
 | ||
|      "e f" '("function" . eval-defun)
 | ||
|      "e r" '("region" . eval-region)
 | ||
|      "e e" '("last s-exp" . eval-last-sexp)
 | ||
|      "e p" '("print s-exp" . ha-eval-print-last-sexp))
 | ||
| #+end_src
 | ||
| ** Ligatures
 | ||
| The idea of using math symbols for a programming languages keywords is /cute/, but can be confusing, so I use it sparingly:
 | ||
| #+begin_src emacs-lisp
 | ||
|   (defun ha-prettify-prog ()
 | ||
|     "Extends the `prettify-symbols-alist' for programming."
 | ||
|     (mapc (lambda (pair) (push pair prettify-symbols-alist))
 | ||
|           '(("lambda" . "𝝀")
 | ||
|             (">=" . "≥")
 | ||
|             ("<=" . "≤")
 | ||
|             ("!=" . "≠")))
 | ||
|     (prettify-symbols-mode))
 | ||
| 
 | ||
|   (add-hook 'prog-mode-hook 'ha-prettify-prog)
 | ||
| #+end_src
 | ||
| Hopefully I can follow [[https://www.masteringemacs.org/article/unicode-ligatures-color-emoji][Mickey Petersen's essay]] on getting full ligatures working, but right now, they don’t work on the Mac, and that is my current workhorse.
 | ||
| ** 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-shell-send command project-dir)))
 | ||
| #+end_src
 | ||
| 
 | ||
| And what about sending the command to Eshell as well?
 | ||
| #+begin_src emacs-lisp
 | ||
|   (defvar rx-compile-to-eshell (rx "|" (0+ space) "s" (0+ space) line-end))
 | ||
| 
 | ||
|   (defun ha-compile-eshell (full-command &optional project-dir)
 | ||
|     "Send a command to the currently running Eshell terminal.
 | ||
|   If a terminal isn't running, it will be started, allowing follow-up
 | ||
|   commands."
 | ||
|     (unless project-dir
 | ||
|       (setq project-dir (projectile-project-name)))
 | ||
| 
 | ||
|     (let ((command (replace-regexp-in-string rx-compile-to-eshell "" full-command)))
 | ||
|       (ha-eshell-send command project-dir)))
 | ||
| #+end_src
 | ||
| And let’s add it to the Project leader:
 | ||
| #+begin_src emacs-lisp
 | ||
|   (ha-leader "p C" 'ha-project-compile)
 | ||
| #+end_src
 | ||
| Note that =p c= (to call [[help:recompile][recompile]]) should still work.
 | ||
| 
 | ||
| Other people’s projects:
 | ||
|   - [[https://github.com/Olivia5k/makefile-executor.el][makefile-executor.el]] :: works only with Makefiles
 | ||
|   - [[https://github.com/tarsius/imake][imake]] :: works only with Makefiles that are formatted with a =help:= target
 | ||
|   - [[https://github.com/emacs-taskrunner/emacs-taskrunner][Taskrunner project]] :: requires ivy or helm, but perhaps I could use the underlying infrastructure to good ol’ [[help:completing-read][completing-read]]
 | ||
| 
 | ||
| Note: Someday I may want to convert my =Makefile= projects to [[https://taskfile.dev/][Taskfile]].
 | ||
| *** Makefile Completion
 | ||
| This magic script is what Bash uses for completion when you type =make= and hit the TAB:
 | ||
| #+name: make-targets
 | ||
| #+begin_src shell :tangle no
 | ||
| make -qRrp : 2> /dev/null | awk -F':' '/^[a-zA-Z0-9][^$#\\/\\t=]*:([^=]|$)/ {split($1,A,/ /);for(i in A)print A[i]}'
 | ||
| #+end_src
 | ||
| 
 | ||
| Which makes it easy to get a list of completions for my compile function:
 | ||
| #+begin_src emacs-lisp :noweb yes
 | ||
|   (defun ha--makefile-completions ()
 | ||
|     "Returns a list of targets from the Makefile in the current directory."
 | ||
|     (when (file-exists-p "Makefile")
 | ||
|       (--map (format "make -k %s" it)
 | ||
|              (shell-command-to-list "<<make-targets>>"))))
 | ||
| #+end_src
 | ||
| *** Python Tox Completion
 | ||
| Let’s just grab the environments to run:
 | ||
| #+begin_src emacs-lisp
 | ||
|   (defun ha--toxfile-completions ()
 | ||
|     "Returns a list of targets from the tox.ini in the current directory."
 | ||
|     (when (file-exists-p "tox.ini")
 | ||
|       (--map (format "tox -e %s" it)
 | ||
|              (shell-command-to-list "tox -a"))))
 | ||
| #+end_src
 | ||
| * Languages
 | ||
| Simple to configure languages go here. More advanced languages go into their own files… eventually.
 | ||
| ** Configuration Files
 | ||
| So many configuration files to track:
 | ||
| #+begin_src emacs-lisp
 | ||
|   (use-package conf-mode
 | ||
|     :mode (("\\.conf\\'"     . conf-space-mode)
 | ||
|            ("\\.repo\\'"     . conf-unix-mode)
 | ||
|            ("\\.setup.*\\'"  . conf-space-mode)))
 | ||
| #+end_src
 | ||
| ** JSON
 | ||
| While interested in the [[https://github.com/emacs-tree-sitter/tree-sitter-langs][tree-sitter]] extensions for JSON, e.g. =json-ts-mode=, that comes with Emacs 29, I’ll deal with what is bundled now.
 | ||
| ** Markdown
 | ||
| All the READMEs and other documentation use [[https://jblevins.org/projects/markdown-mode/][markdown-mode]].
 | ||
| #+begin_src emacs-lisp
 | ||
|   (use-package markdown-mode
 | ||
|     :straight (:host github :repo "jrblevin/markdown-mode")
 | ||
|     :mode ((rx ".md" string-end) . gfm-mode)
 | ||
|     :init (setq markdown-command "multimarkdown")
 | ||
|     :general
 | ||
|     (:states 'normal :no-autoload t :keymaps 'markdown-mode-map
 | ||
|              "SPC m l" '("insert link" . markdown-insert-link)
 | ||
|              ;; SPC u 3 SPC m h for a third-level header:
 | ||
|              "SPC m h" '("insert header" . markdown-insert-header-dwim)
 | ||
|              "SPC m e" '("export" . markdown-export)
 | ||
|              "SPC m p" '("preview" . markdown-export-and-preview)))
 | ||
| #+end_src
 | ||
| Note that the markdown-specific commands use the ~C-c C-c~ and  ~C-c C-s~ prefixes.
 | ||
| ** Ansible
 | ||
| Doing a lot of [[https://github.com/yoshiki/yaml-mode][YAML work]], but this project needs a new maintainer.
 | ||
| #+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
 | ||
| Note this needs the following to run properly:
 | ||
| #+begin_src sh
 | ||
|   pip install yamllint
 | ||
| #+end_src
 | ||
| 
 | ||
| Ansible uses Jinja, 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
 | ||
| 
 | ||
| Do I consider all YAML files an Ansible file needing [[https://github.com/k1LoW/emacs-ansible][ansible-mode]]?
 | ||
| #+begin_src emacs-lisp
 | ||
|   (use-package ansible
 | ||
|     :init
 | ||
|     (setq ansible-vault-password-file "~/.ansible-vault-passfile")
 | ||
|     ;; :hook (yaml-mode . ansible-mode)
 | ||
|     :config
 | ||
|     (ha-leader "t y" 'ansible))
 | ||
| #+end_src
 | ||
| The [[help:ansible-vault-password-file][ansible-vault-password-file]] variable needs to change /per project/, so let’s use the =.dir-locals.el= file, for instance:
 | ||
| #+begin_src emacs-lisp :tangle no
 | ||
|   ((nil . ((ansible-vault-password-file . "playbooks/.vault-password"))))
 | ||
| 
 | ||
| #+end_src
 | ||
| 
 | ||
| The YAML files get access Ansible’s documentation using the [[https://github.com/emacsorphanage/ansible-doc][ansible-doc]] project:
 | ||
| #+begin_src emacs-lisp
 | ||
|   (use-package ansible-doc
 | ||
|     :hook (yaml-mode . ansible-doc-mode)
 | ||
|     :config
 | ||
|     (ha-local-leader :keymaps 'yaml-mode-map
 | ||
|       "d"  '(:ignore t :which-key "docs")
 | ||
|       "d d" 'ansible-doc))
 | ||
| #+end_src
 | ||
| 
 | ||
| The [[https://github.com/emacsmirror/poly-ansible][poly-ansible]] project uses [[https://polymode.github.io/][polymode]], gluing [[https://github.com/paradoxxxzero/jinja2-mode][jinja2-mode]] into [[https://github.com/yoshiki/yaml-mode][yaml-mode]].
 | ||
| #+begin_src emacs-lisp :tangle no
 | ||
|   (use-package polymode)
 | ||
| 
 | ||
|   (use-package poly-ansible
 | ||
|     :after polymode
 | ||
|     :straight (:host github :repo "emacsmirror/poly-ansible")
 | ||
|     :hook ((yaml-mode . poly-ansible-mode)
 | ||
|            (poly-ansible-mode . font-lock-update)))
 | ||
| #+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]])?
 | ||
| 
 | ||
| First, use =npm= to install the program:
 | ||
| #+begin_src sh
 | ||
|   npm installl -g @ansible/ansible-language-server
 | ||
| #+end_src
 | ||
| ** Shell Scripts
 | ||
| While I don't like writing them, I can't get away from them. Check out the goodies in [[https://www.youtube.com/watch?v=LTC6SP7R1hA&t=5s][this video]].
 | ||
| 
 | ||
| While filename extensions work fine most of the time, I don't like to pre-pend =.sh= to the shell scripts I write, and instead, would like to associate =shell-mode= with all files in a =bin= directory:
 | ||
| #+begin_src emacs-lisp
 | ||
|   (use-package sh-mode
 | ||
|     :straight (:type built-in)
 | ||
|     :mode (rx (or (seq ".sh" eol)
 | ||
|                   "/bin/"))
 | ||
|     :init
 | ||
|     (setq sh-basic-offset 2
 | ||
|           sh-indentation 2)
 | ||
|     :config
 | ||
|     (ha-auto-insert-file (rx (or (seq ".sh" eol)
 | ||
|                                  "/bin/"))
 | ||
|                          "sh-mode.sh")
 | ||
|     :hook
 | ||
|     (after-save . executable-make-buffer-file-executable-if-script-p))
 | ||
| #+end_src
 | ||
| *Note:* we make the script /executable/ by default. See [[https://emacsredux.com/blog/2021/09/29/make-script-files-executable-automatically/][this essay]] for details, but it turns on the executable bit if the script has a shebang at the top of the file.
 | ||
| 
 | ||
| The [[https://www.shellcheck.net/][shellcheck]] project integrates with [[Flycheck]]. First, install the executable into the system, for instance, on a Mac:
 | ||
| #+begin_src sh
 | ||
|   brew install shellcheck
 | ||
| #+end_src
 | ||
| And we can enable it:
 | ||
| #+begin_src emacs-lisp
 | ||
|   (flycheck-may-enable-checker 'sh-shellcheck)
 | ||
| #+end_src
 | ||
| Place the following /on a line/ before a shell script warning to ignore it:
 | ||
| #+begin_src sh
 | ||
| # shellcheck disable=SC2116,SC2086
 | ||
| #+end_src
 | ||
| See [[https://github.com/koalaman/shellcheck/wiki/Ignore][this page]] for details.
 | ||
| 
 | ||
| Integration with the [[https://github.com/bash-lsp/bash-language-server][Bash LSP implementation]]. First, install that too:
 | ||
| #+begin_src sh
 | ||
|   brew install bash-language-server
 | ||
| #+end_src
 | ||
| ** Fish Shell
 | ||
| I think the [[https://fishshell.com/][fish shell]] is an interesting experiment (and I appreciate the basics that come with [[https://github.com/emacsmirror/fish-mode][fish-mode]]).
 | ||
| #+begin_src emacs-lisp
 | ||
|   (use-package fish-mode
 | ||
|     :mode (rx ".fish" eol)
 | ||
|     :config
 | ||
|     (ha-auto-insert-file (rx ".fish") "fish-mode.sh")
 | ||
|     :hook
 | ||
|     (fish-mode . (lambda () (add-hook 'before-save-hook 'fish_indent-before-save))))
 | ||
| #+end_src
 | ||
| * Technical Artifacts                                :noexport:
 | ||
| Provide a name to =require= this code.
 | ||
| #+begin_src emacs-lisp :exports none
 | ||
|   (provide 'ha-programming)
 | ||
|   ;;; ha-programming.el ends here
 | ||
| #+end_src
 | ||
| 
 | ||
| Before you can build this on a new system, make sure that you put the cursor over any of these properties, and hit: ~C-c C-c~
 | ||
| 
 | ||
| #+DESCRIPTION: A literate programming file for helping me program.
 | ||
| 
 | ||
| #+PROPERTY:    header-args:sh :tangle no
 | ||
| #+PROPERTY:    header-args:emacs-lisp yes
 | ||
| #+PROPERTY:    header-args    :results none :eval no-export :comments no mkdirp yes
 | ||
| 
 | ||
| #+OPTIONS:     num:nil toc:nil todo:nil tasks:nil tags:nil date:nil
 | ||
| #+OPTIONS:     skip:nil author:nil email:nil creator:nil timestamp:nil
 | ||
| #+INFOJS_OPT:  view:nil toc:nil ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js
 |