hamacs/ha-programming.org
Howard Abrams 264720be8e Wrote functions to change from camelCase to snake_case.
Need to also do something for dashed case (Lisp case?).
2023-07-05 10:22:19 -07:00

54 KiB
Raw Blame History

General Programming Configuration

A literate programming file for helping me program.

Introduction

Configuration for programming interfaces and workflows that behave similarly.

General

The following work for all programming languages.

direnv

Farm off commands into virtual environments:

  (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))

Spell Checking Comments

The flyspell-prog-mode checks for misspellings in comments.

  (use-package flyspell
    :hook (prog-mode . flyspell-prog-mode))

Flycheck

Why use flycheck over the built-in flymake? Speed used to be the advantage, but Im now pushing much of this to LSP, so speed is less of an issue. What about when I am not using LSP? Also, since Ive hooked grammar checkers, I need this with global keybindings.

  (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)))

Documentation

Im interested in using 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 devdocs project is active, and seems to work well. Its advantage is a special mode for moving around the documentation.

  (use-package devdocs
    :general (:states 'normal
      "gD" '("devdocs" . 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)))

The 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 cant search for a function, until you download its pack. This is slightly faster because of this.

  (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)))

Code Folding

While Emacs has options for viewing and moving around code, sometimes, we could collapse all functions, and then start to expand them one at a time. For this, we could enable the built-in hide-show feature:

  (use-package hide-show
    :straight (:type built-in)
    :init
    (setq hs-hide-comments t
          hs-hide-initial-comment-block t
          hs-isearch-open t)
    :hook (prog-mode . hs-minor-mode))

Note that hide-show doesnt work with complex YAML files. The origami mode works better out-of-the-box, as it works with Python and Lisp, but falls back to indents as the format, which works well.

  (use-package origami
    :init
    (setq origami-fold-replacement "⤵")
    :hook (prog-mode . origami-mode))

To take advantage of this, type:

z m
To collapse everything
z r
To open everything
z o
To open a particular section
z c
To collapse a section (like a function)
z a
Toggles open to close

Note: Yes, we could use vimish-fold (and its cousin, evil-vimish-fold) and well see if I need those.

Smart Parenthesis

We need to make sure we keep the smartparens project always in strict mode, because who wants to worry about paren-matching:

  (use-package smartparens
    :custom
    (smartparens-global-strict-mode t)

    :config
    (sp-with-modes sp-lisp-modes
      ;; disable ', as it's the quote character:
      (sp-local-pair "'" nil :actions nil))

    (sp-with-modes (-difference sp-lisp-modes sp-clojure-modes)
      ;; use the pseudo-quote inside strings where it serve as hyperlink.
      (sp-local-pair "`" "'"
                     :when '(sp-in-string-p
                             sp-in-comment-p)
                     :skip-match (lambda (ms _mb _me)
                                   (cond
                                    ((equal ms "'") (not (sp-point-in-string-or-comment)))
                                    (t (not (sp-point-in-string-or-comment)))))))
    :hook
    (prog-mode . smartparens-strict-mode))

Navigation

Move by Functions

The mark-paragraph and downcase-word isnt very useful in a programming context, and makes more sense to use them to jump around function-by-function:

  (evil-define-key '(normal insert emacs) prog-mode-map
    (kbd "M-h")    'beginning-of-defun
    (kbd "M-l")    'beginning-of-next-defun)

But one of those functions doesnt exist:

  (defun beginning-of-next-defun (count)
    "Move to the beginning of the following function."
    (interactive "P")
    (end-of-defun count)
    (end-of-defun)
    (beginning-of-defun))

Tree Sitter

Install the binary for the tree-sitter project. For instance:

  brew install tree-sitter npm # Since most support packages need that too.

The tree-sitter project does not install any language grammars by default—after all, it would have no idea which particular languages to parse and analyze!

Next, using the tree-sitter command line tool, create the config.json file:

  tree-sitter init-config

Normally, you would need to add all the projects to directory clones in ~/src, e.g.

  while read REPO
  do
    LOCATION=~/src/$(basename ${REPO})
    if [ ! -d ${LOCATION} ]
    then
      git clone ${REPO} ${LOCATION}
    fi
    cd ${LOCATION}
    git pull origin
    npm install
  done <<EOL
  https://github.com/tree-sitter/tree-sitter-css
  https://github.com/tree-sitter/tree-sitter-json
  https://github.com/tree-sitter/tree-sitter-python
  https://github.com/tree-sitter/tree-sitter-bash
  https://github.com/tree-sitter/tree-sitter-ruby
  https://github.com/camdencheek/tree-sitter-dockerfile
  https://github.com/alemuller/tree-sitter-make
  https://github.com/ikatyang/tree-sitter-yaml
  https://github.com/Wilfred/tree-sitter-elisp
  EOL

The npm install usually works, but I may work on some sort of various process, for instance:

  for TSS in ~/src/tree-sitter-*
  do
    cd $TSS
    npm install || cargo build || make install   # Various build processes!?
  done

At this point, we can now parse stuff using: tree-sitter parse <source-code-file>

However, Emacs already has the ability to download and install grammars, so following instructions from Mickey Petersens essay on using Tree-sitter with Combobulate:

  (when (string-search "TREE_SITTER" system-configuration-features)
    (use-package treesit
      :straight (:type built-in)
      :preface
      (defun mp-setup-install-grammars ()
        "Install Tree-sitter grammars if they are absent."
        (interactive)
        (sit-for 10)
        (dolist (grammar
                 '((bash          "https://github.com/tree-sitter/tree-sitter-bash")
                   (make          "https://github.com/alemuller/tree-sitter-make")
                   (css           "https://github.com/tree-sitter/tree-sitter-css")
                   (json          "https://github.com/tree-sitter/tree-sitter-json")
                   (html          "https://github.com/tree-sitter/tree-sitter-html")
                   ;; (javascript "https://github.com/tree-sitter/tree-sitter-javascript" "master" "src")
                   (python        "https://github.com/tree-sitter/tree-sitter-python")
                   (ruby          "https://github.com/tree-sitter/tree-sitter-ruby")
                   (yaml          "https://github.com/ikatyang/tree-sitter-yaml")))
          (add-to-list 'treesit-language-source-alist grammar)
          (treesit-install-language-grammar (car grammar))))

      ;; Optional, but recommended. Tree-sitter enabled major modes are
      ;; distinct from their ordinary counterparts.
      ;;
      ;; You can remap major modes with `major-mode-remap-alist'. Note
      ;; this does *not* extend to hooks! Make sure you migrate them also
      (dolist (mapping '((css-mode    . css-ts-mode)
                         (json-mode   . json-ts-mode)
                         ;; (makefile-mode . makefile-ts-mode)
                         (python-mode . python-ts-mode)
                         (ruby-mode   . ruby-ts-mode)
                         (sh-mode     . bash-ts-mode)
                         (yaml-mode   . yaml-ts-mode)))
        (add-to-list 'major-mode-remap-alist mapping))

      :config
      (mp-setup-install-grammars)))

Combobulate

I like Clever Parenthesis, but can we extend that to other languages generally? After reading Mickey Petersens essay, Combobulate project, I decided to try out his combobulate package. Of course, this can only work with the underlying tooling supplied by the Tree Sitter

  (when (string-search "TREE_SITTER" system-configuration-features)
    (use-package combobulate
      :straight (:host github :repo "mickeynp/combobulate")
      :after treesit
      :hook ((css-ts-mode    . combobulate-mode)
             (json-ts-mode   . combobulate-mode)
             (python-ts-mode . combobulate-mode)
             (yaml-ts-mode   . combobulate-mode))))

I can create a helper function to allow me to jump to various types of—well, types:

  (when (string-search "TREE_SITTER" system-configuration-features)
    (use-package combobulate
        :config
        (defun ha-comb-jump (&rest tree-sitter-types)
          "Use `avy' to jump to a particular type of element.6 "
          (lexical-let ((types tree-sitter-types))
            (lambda ()
              (interactive)
              (with-navigation-nodes (:nodes types)
                                     (combobulate-avy-jump)))))))

Now, I can create an interface of keystrokes to jump around like a boss:

  (when (string-search "TREE_SITTER" system-configuration-features)
    (use-package combobulate
      :general
      (:states 'visual :keymaps 'combobulate-key-map
               "o" '("mark node" . combobulate-mark-node-dwim))              ; Mark symbol since "o" doesn't do anything
      (:states 'normal :keymaps 'combobulate-key-map
               "g J" '("avy jump" . combobulate-avy)
               "[ [" '("prev node" . combobulate-navigate-logical-previous)
               "] ]" '("next node" . combobulate-navigate-logical-next)
               "[ f" '("prev defun" . combobulate-navigate-beginning-of-defun)
               "] f" '("next defun" . combobulate-navigate-end-of-defun)

               "[ m" '("drag back" . combobulate-drag-up)
               "] m" '("drag forward" . combobulate-drag-down)
               "[ r" '("raise" . combobulate-vanish-node)

               "g j" '(:ignore t :which-key "combobulate jump")
               "g j j" '("all" . combobulate-avy-jump)
               "g j s" `("strings" . ,(ha-comb-jump "string"))
               "g j c" `("comments" . ,(ha-comb-jump "comment"))
               "g j i" `("conditionals" . ,(ha-comb-jump "conditional_expression" "if_statement"
                                                         "if_clause" "else_clause" "elif_clause" ))
               "g j l" `("loops" . ,(ha-comb-jump "for_statement" "for_in_clause" "while_statement"
                                                  "list_comprehension" "dictionary_comprehension" "set_comprehension"))
               "g j f" '("functions" . combobulate-avy-jump-defun))))

Mickeys interface is the 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 text objects can be better too with the evil-textobj-tree-sitter project:

  (when (string-search "TREE_SITTER" system-configuration-features)
    (use-package evil-textobj-tree-sitter
      :config
      ;; We need to bind keys to the text objects found at:
      ;; https://github.com/nvim-treesitter/nvim-treesitter-textobjects#built-in-textobjects

      ;; bind `function.outer`(entire function block) to `f` for use in things like `vaf`, `yaf`
      (define-key evil-outer-text-objects-map "f" (evil-textobj-tree-sitter-get-textobj "function.outer"))
      ;; bind `function.inner`(function block without name and args) to `f` for use in things like `vif`, `yif`
      (define-key evil-inner-text-objects-map "f" (evil-textobj-tree-sitter-get-textobj "function.inner"))

      (define-key evil-outer-text-objects-map "c" (evil-textobj-tree-sitter-get-textobj "comment.outer"))
      (define-key evil-inner-text-objects-map "c" (evil-textobj-tree-sitter-get-textobj "comment.inner"))
      (define-key evil-outer-text-objects-map "i" (evil-textobj-tree-sitter-get-textobj "conditional.outer"))
      (define-key evil-inner-text-objects-map "i" (evil-textobj-tree-sitter-get-textobj "conditional.inner"))
      (define-key evil-outer-text-objects-map "b" (evil-textobj-tree-sitter-get-textobj "loop.outer"))
      (define-key evil-inner-text-objects-map "b" (evil-textobj-tree-sitter-get-textobj "loop.inner"))))

Seems the macro, evil-textobj-tree-sitter-get-textobj has a bug, so the following—which would have been easier to write—doesnt work:

  (dolist (combo '(("f" "function.outer" "function.inner")
                   ("b" "loop.outer" "loop.inner")
                   ;; ...
                   ("c" "comment.outer" "comment.inner")))
    (destructuring-bind (key outer inner) combo
      ;; bind an outer (e.g. entire function block) for use in things like `vaf`, `yaf` combo
      (define-key evil-outer-text-objects-map key (evil-textobj-tree-sitter-get-textobj outer))
      ;; bind an inner (e.g. function block without name and args) for use in things like `vif`, `yif`
      (define-key evil-inner-text-objects-map key (evil-textobj-tree-sitter-get-textobj inner))))

dumb-jump

Once upon a time, we use to create a TAGS file that contained the database for navigating code bases, but with new faster versions of grep, e.g. ack, ag (aka, the Silver Searcher), ugrep and ripgrep, we should be able to use them. but I want to:

  • Be in a function, and see its callers. For this, the rg-dwim function is my bread-and-butter.
  • Be on a function, and jump to the definition. For this, I use dumb-jump, which uses the above utilities.
  (use-package dumb-jump
    :config
    (setq dumb-jump-prefer-searcher 'rg
          xref-history-storage #'xref-window-local-history
          xref-show-definitions-function #'xref-show-definitions-completing-read)

    (add-hook 'xref-backend-functions #'dumb-jump-xref-activate)

    ;; 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)))

I have two different jumping systems, the Xref interface and Evils. While comparable goals, they are behave different. Lets compare evil keybindings:

M-. g . xref-find-definitions (also g d for evil-goto-definition)†
g > xref-find-definitions-other-window
M-, g , xref-go-back (see xref-pop-marker-stack)
C-M-, g < xref-go-forward (kinda like xref-find-definitions)
M-? g / xref-find-references to go from definition to code calls‡
g ? xref-find-references-and-replace could be more accurate than iEdit.
C-M-. g h xref-find-apropos … doesnt work well without LSP
C-TAB perform completion around point (also M-TAB), see Auto Completion.

† Prefix to prompt for the term \ ‡ If it finds more than one definition, Emacs displays the xref buffer, allowing you to select the definition.

Language Server Protocol (LSP) Integration

The LSP is a way to connect editors (like Emacs) to languages (like Lisp)… wait, no. While originally designed for VS Code and probably Python, we can abstract away Jedi and the Emacs integration to Jedi (and duplicate everything for Ruby, and Clojure, and…).

Emacs has two LSP projects, and while I have used /git/howard/hamacs/src/commit/264720be8ea9bde2ac8c8f40aa1708fecd9051d0/LSP%20Mode, but since I dont have heavy IDE requirements, I am finding that /git/howard/hamacs/src/commit/264720be8ea9bde2ac8c8f40aa1708fecd9051d0/eglot to be simpler.

LSP

  (use-package lsp-mode
    :commands (lsp lsp-deferred)
    :init
    ;; Let's make lsp-doctor happy with these settings:
    (setq gc-cons-threshold (* 100 1024 1024)
          read-process-output-max (* 1024 1024)
          company-idle-delay 0.0 ; Are thing fast enough to do this?
          lsp-keymap-prefix "s-m")

    :config
    (global-set-key (kbd "s-m") 'lsp)
    (ha-local-leader :keymaps 'prog-mode-map
      "w"  '(:ignore t :which-key "lsp")
      "l"  '(:ignore t :which-key "lsp")
      "ws" '("start" . lsp))

    ;; The following leader-like keys, are only available when I have
    ;; started LSP, and is an alternate to Command-m:
    :general
    (:states 'normal :keymaps 'lsp-mode-map
             ", w r" '("restart"  . lsp-reconnect)
             ", w b" '("events"   . lsp-events-buffer)
             ", w e" '("errors"   . lsp-stderr-buffer)
             ", w q" '("quit"     . lsp-shutdown)
             ", w Q" '("quit all" . lsp-shutdown-all)

             ", l r" '("rename"   . lsp-rename)
             ", l f" '("format"   . lsp-format)
             ", l a" '("actions"  . lsp-code-actions)
             ", l i" '("imports"  . lsp-code-action-organize-imports)
             ", l d" '("doc"      . lsp-lookup-documentation))

   :hook ((lsp-mode . lsp-enable-which-key-integration)))

I will want to start adding commands under my , mode-specific key sequence leader, but in the meantime, all LSP-related keybindings are available under ⌘-m. See this page for the default keybindings.

UI

The lsp-ui project offers much of the display and interface to LSP. Seems to make the screen cluttered.

  (use-package lsp-ui
    :commands lsp-ui-mode
    :config
    (setq lsp-ui-sideline-ignore-duplicate t
          lsp-ui-sideline-show-hover t
          lsp-ui-sideline-show-diagnostics t)
    :hook (lsp-mode . lsp-ui-mode))

Treemacs

  (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))

Company Completion

The company-lsp offers a company completion backend for lsp-mode:

  (use-package company-lsp
    :config
    (push 'company-lsp company-backends))

To options that might be interesting:

  • company-lsp-async: When set to non-nil, fetch completion candidates asynchronously.
  • company-lsp-enable-snippet: Set it to non-nil if you want to enable snippet expansion on completion. Set it to nil to disable this feature.

iMenu

The lsp-imenu project offers a lsp-ui-imenu function for jumping to functions:

  (use-package lsp-ui-imenu
      :straight nil
      :after lsp-ui
      :config
      (ha-local-leader :keymaps 'prog-mode-map
        "g"  '(:ignore t :which-key "goto")
        "g m" '("imenu" . lsp-ui-imenu))
      (add-hook 'lsp-after-open-hook 'lsp-enable-imenu))

Display Configuration

Using the Doom Modeline to add notifications:

  (use-package doom-modeline
    :config
    (setq doom-modeline-lsp t
          doom-modeline-env-version t))

Function Call Notifications

As I've mentioned on my website, I've created a beep function that notifies when long running processes complete.

  (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)))

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, iedit is often sufficient.

  (use-package iedit
    :config
    (ha-leader "s e" '("iedit" . iedit-mode)))

Case Conversion

Sure we can upper and lowercase stuff, but can we change from Camel to Snake case and back? For instance, fooBarBaz, would with z s keybinding, change to foo_bar_baz (and repeating that sequence would return it to fooBarBaz).

  (defun get-bounds-of-symbol ()
    "Return a list of the start and end of the current symbol at point.
  Return the start and end of region, if the region is active."
   (if (region-active-p)
        (cons (region-beginning) (region-end))
     (bounds-of-thing-at-point 'symbol)))

  (defun camel-to-snake-case ()
    "Convert the active region or current symbol to snake case.
  For instance, fooBarBaz will be foo_bar_baz."
    (interactive)
    (cl-destructuring-bind (start . end) (get-bounds-of-symbol)
      (save-excursion
        (goto-char start)
        (narrow-to-region start end)
        (let ((case-fold-search nil))
          (while (re-search-forward (rx (group (one-or-more upper))) nil t)
            (replace-match (concat "_" (downcase (match-string 1))) t nil)))
        (widen))))

  (defun snake-to-camel-case ()
    "Convert the active region or current symbol to camel case.
  For instance, foo_bar_baz will be fooBarBaz."
    (interactive)
    (cl-destructuring-bind (start . end) (get-bounds-of-symbol)
      (save-excursion
        (goto-char start)
        (narrow-to-region start end)
        (let ((case-fold-search nil))
          (while (re-search-forward (rx "_" (group  alpha)) nil t)
            (replace-match (upcase (match-string 1)) t nil)))
        (widen))))

  (defun toggle-snake-to-camel-case ()
    "Toggle current symbol from snake to camel, and visa-versa."
    (interactive)
    (cl-destructuring-bind (start . end) (get-bounds-of-symbol)
      (save-excursion
        (goto-char start)
        (if (looking-at (rx (zero-or-more alphanumeric) "_"))
            (snake-to-camel-case)
          (camel-to-snake-case)))))

And we can put this on the z keybinding:

  (use-package evil
    :general
    (:states '(normal visual motion operator)
             "z s" '("toggle snake/camel" . toggle-snake-to-camel-case)
             "z S" '("to snake case" . camel-to-snake-case)
             "z C" '("to camel case" . snake-to-camel-case)))

I would like to have this bound on the g sequence, but that is crowded.

Commenting

I like comment-dwim (M-;), and I like comment-box, but I have an odd personal style that I like to codify:

  (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)))

And a keybinding:

  (ha-local-leader :keymaps 'prog-mode-map
    "c"  '(:ignore t :which-key "comment")
    "c l" '("comment line" . ha-comment-line))

Evaluation

While I like eval-print-last-sexp, I would like a bit of formatting in order to keep the results in the file.

  (defun ha-eval-print-last-sexp (&optional internal-arg)
    "Evaluate the expression located before the point.
  Insert results back into the buffer at the end of the line after
  a comment."
    (interactive)
    (save-excursion
      (eval-print-last-sexp internal-arg))
    (end-of-line)
    (insert "  ")
    (insert comment-start)
    (insert "⟹ ")
    (dotimes (i 2)
      (next-line)
      (join-line)))

Typical keybindings for all programming modes:

  (ha-local-leader :keymaps 'prog-mode-map
     "e"  '(:ignore t :which-key "eval")
     "e ;" '("expression" . eval-expression)
     "e b" '("buffer" . eval-buffer)
     "e f" '("function" . eval-defun)
     "e r" '("region" . eval-region)
     "e e" '("last s-exp" . eval-last-sexp)
     "e p" '("print s-exp" . ha-eval-print-last-sexp))

Ligatures

The idea of using math symbols for a programming languages keywords is cute, but can be confusing, so I use it sparingly:

  (defun ha-prettify-prog ()
    "Extends the `prettify-symbols-alist' for programming."
    (mapc (lambda (pair) (push pair prettify-symbols-alist))
          '(("lambda" . "𝝀")
            (">=" . "≥")
            ("<=" . "≤")
            ("!=" . "≠")))
    (prettify-symbols-mode))

  (add-hook 'prog-mode-hook 'ha-prettify-prog)

Hopefully I can follow Mickey Petersen's essay on getting full ligatures working, but right now, they dont work on the Mac, and that is my current workhorse.

  (use-package ligature
    :config
    ;; Enable the "www" ligature in every possible major mode
    (ligature-set-ligatures 't '("www"))

    ;; Enable traditional ligature support in eww-mode, if the
    ;; `variable-pitch' face supports it
    (ligature-set-ligatures '(org-mode eww-mode) '("ff" "fi" "ffi"))

    (ligature-set-ligatures '(html-mode nxml-mode web-mode)
                            '("<!--" "-->" "</>" "</" "/>" "://"))

    ;; Create a new ligature:
    (ligature-set-ligatures 'markdown-mode '(("=" (rx (+ "=") (? (| ">" "<"))))
                                             ("-" (rx (+ "-")))))

    ;; Enable all Cascadia Code ligatures in programming modes
    (ligature-set-ligatures 'prog-mode '("|||>" "<|||" "<==>" "<!--" "####" "~~>" "***" "||=" "||>"
                                         ":::" "::=" "=:=" "===" "==>" "=!=" "=>>" "=<<" "=/=" "!=="
                                         "!!." ">=>" ">>=" ">>>" ">>-" ">->" "->>" "-->" "---" "-<<"
                                         "<~~" "<~>" "<*>" "<||" "<|>" "<$>" "<==" "<=>" "<=<" "<->"
                                         "<--" "<-<" "<<=" "<<-" "<<<" "<+>" "</>" "###" "#_(" "..<"
                                         "..." "+++" "/==" "///" "_|_" "www" "&&" "^=" "~~" "~@" "~="
                                         "~>" "~-" "**" "*>" "*/" "||" "|}" "|]" "|=" "|>" "|-" "{|"
                                         "[|" "]#" "::" ":=" ":>" ":<" "$>" "==" "=>" "!=" "!!" ">:"
                                         ">=" ">>" ">-" "-~" "-|" "->" "--" "-<" "<~" "<*" "<|" "<:"
                                         "<$" "<=" "<>" "<-" "<<" "<+" "</" "#{" "#[" "#:" "#=" "#!"
                                         "##" "#(" "#?" "#_" "%%" ".=" ".-" ".." ".?" "+>" "++" "?:"
                                         "?=" "?." "??" ";;" "/*" "/=" "/>" "//" "__" "~~" "(*" "*)"
                                         "\\\\" "://"))
    ;; Enables ligature checks globally in all buffers. You can also do it
    ;; per mode with `ligature-mode'.
    (global-ligature-mode t))

Until I can get Harfbuzz support on my Emacs-Plus build of Mac, the following work-around seems to mostly work:

  (defun ha-mac-litagure-workaround ()
    "Implement an old work-around for ligature support.
  This kludge seems to only need to be set for my Mac version of
  Emacs, since I can't build it with Harfuzz support."
    (let ((alist '((33 . ".\\(?:\\(?:==\\|!!\\)\\|[!=]\\)")
                   (35 . ".\\(?:###\\|##\\|_(\\|[#(?[_{]\\)")
                   (36 . ".\\(?:>\\)")
                   (37 . ".\\(?:\\(?:%%\\)\\|%\\)")
                   (38 . ".\\(?:\\(?:&&\\)\\|&\\)")
                   (42 . ".\\(?:\\(?:\\*\\*/\\)\\|\\(?:\\*[*/]\\)\\|[*/>]\\)")
                   (43 . ".\\(?:\\(?:\\+\\+\\)\\|[+>]\\)")
                   (45 . ".\\(?:\\(?:-[>-]\\|<<\\|>>\\)\\|[<>}~-]\\)")
                   (46 . ".\\(?:\\(?:\\.[.<]\\)\\|[.=-]\\)")
                   (47 . ".\\(?:\\(?:\\*\\*\\|//\\|==\\)\\|[*/=>]\\)")
                   (48 . ".\\(?:x[a-zA-Z]\\)")
                   (58 . ".\\(?:::\\|[:=]\\)")
                   (59 . ".\\(?:;;\\|;\\)")
                   (60 . ".\\(?:\\(?:!--\\)\\|\\(?:~~\\|->\\|\\$>\\|\\*>\\|\\+>\\|--\\|<[<=-]\\|=[<=>]\\||>\\)\\|[*$+~/<=>|-]\\)")
                   (61 . ".\\(?:\\(?:/=\\|:=\\|<<\\|=[=>]\\|>>\\)\\|[<=>~]\\)")
                   (62 . ".\\(?:\\(?:=>\\|>[=>-]\\)\\|[=>-]\\)")
                   (63 . ".\\(?:\\(\\?\\?\\)\\|[:=?]\\)")
                   (91 . ".\\(?:]\\)")
                   (92 . ".\\(?:\\(?:\\\\\\\\\\)\\|\\\\\\)")
                   (94 . ".\\(?:=\\)")
                   (119 . ".\\(?:ww\\)")
                   (123 . ".\\(?:-\\)")
                   (124 . ".\\(?:\\(?:|[=|]\\)\\|[=>|]\\)")
                   (126 . ".\\(?:~>\\|~~\\|[>=@~-]\\)"))))
      (dolist (char-regexp alist)
        (set-char-table-range composition-function-table (car char-regexp)
                              `([,(cdr char-regexp) 0 font-shape-gstring])))))

  (unless (s-contains? "HARFBUZZ" system-configuration-features)
    (add-hook 'prog-mode-hook #'ha-mac-litagure-workaround))

The unicode-fonts package rejigs the internal tables Emacs uses to pick better fonts for unicode codepoint ranges.

  (use-package unicode-fonts
    :config
    (unicode-fonts-setup))

Compiling

The compile function lets me enter a command to run, or I can search the history for a previous run. What it doesnt give me, is a project-specific list of commands. Perhaps, for each project, I define in .dir-locals.el a variable, compile-command-list, like:

  ((nil . ((compile-command . "make -k ")
           (compile-command-list . ("ansible-playbook playbooks/confluence_test.yml"
                                "ansible-playbook playbooks/refresh_inventory.yml")))))

To make the compile-command-list variable less risky, we need to declare it:

  (defvar compile-command-list nil "A list of potential commands to give to `ha-project-compile'.")

  (defun ha-make-compile-command-list-safe ()
    "Add the current value of `compile-command-list' safe."
    (interactive)
    (add-to-list 'safe-local-variable-values `(compile-command-list . ,compile-command-list)))

What compile commands should I have on offer? Along with the values in compile-command-list (if set), I could look at files in the projects root and get targets from a Makefile, etc. Well use helper functions I define later:

  (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))))

My replacement to compile uses my new completing-read function:

  (defun ha-project-compile (command)
    "Run `compile' from a list of directory-specific commands."
    (interactive (list (completing-read "Compile command: "
                                        (ha--compile-command-list)
                                        nil nil "" 'compile-history)))
    (let ((default-directory (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)))))

If I end a command with a |v, it sends the compile command to a vterm session for the project, allowing me to continue the commands:

  (defvar rx-compile-to-vterm  (rx "|" (0+ space) "v" (0+ space) line-end))

  (defun ha-compile-vterm (full-command &optional project-dir)
    (unless project-dir
      (setq project-dir (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)))

And what about sending the command to Eshell as well?

  (defvar rx-compile-to-eshell (rx "|" (0+ space) "s" (0+ space) line-end))

  (defun ha-compile-eshell (full-command &optional project-dir)
    "Send a command to the currently running Eshell terminal.
  If a terminal isn't running, it will be started, allowing follow-up
  commands."
    (unless project-dir
      (setq project-dir (projectile-project-name)))

    (let ((command (replace-regexp-in-string rx-compile-to-eshell "" full-command)))
      (ha-eshell-send command project-dir)))

And lets add it to the Project leader:

  (ha-leader "p C" 'ha-project-compile)

Note that p c (to call recompile) should still work.

Other peoples projects:

makefile-executor.el
works only with Makefiles
imake
works only with Makefiles that are formatted with a help: target
Taskrunner project
requires ivy or helm, but perhaps I could use the underlying infrastructure to good ol completing-read

Note: Someday I may want to convert my Makefile projects to Taskfile.

Makefile Completion

This magic script is what Bash uses for completion when you type make and hit the TAB:

make -qRrp : 2> /dev/null | awk -F':' '/^[a-zA-Z0-9][^$#\\/\\t=]*:([^=]|$)/ {split($1,A,/ /);for(i in A)print A[i]}'

Which makes it easy to get a list of completions for my compile function:

  (defun ha--makefile-completions ()
    "Returns a list of targets from the Makefile in the current directory."
    (when (file-exists-p "Makefile")
      (--map (format "make -k %s" it)
             (shell-command-to-list "<<make-targets>>"))))

Python Tox Completion

Lets just grab the environments to run:

  (defun ha--toxfile-completions ()
    "Returns a list of targets from the tox.ini in the current directory."
    (when (file-exists-p "tox.ini")
      (--map (format "tox -e %s" it)
             (shell-command-to-list "tox -a"))))

Languages

Simple to configure languages go here. More advanced languages go into their own files… eventually.

Configuration Files

So many configuration files to track:

  (use-package conf-mode
    :mode (("\\.conf\\'"     . conf-space-mode)
           ("\\.repo\\'"     . conf-unix-mode)
           ("\\.setup.*\\'"  . conf-space-mode)))

JSON

While interested in the tree-sitter extensions for JSON, e.g. json-ts-mode, that comes with Emacs 29, Ill deal with what is bundled now.

Markdown

All the READMEs and other documentation use markdown-mode.

  (use-package markdown-mode
    :straight (:host github :repo "jrblevin/markdown-mode")
    :mode ((rx ".md" string-end) . gfm-mode)
    :init (setq markdown-command "multimarkdown")
    :general
    (:states 'normal :no-autoload t :keymaps 'markdown-mode-map
             ", l" '("insert link" . markdown-insert-link)
             ;; SPC u 3 , h for a third-level header:
             ", h" '("insert header" . markdown-insert-header-dwim)
             ", e" '("export" . markdown-export)
             ", p" '("preview" . markdown-export-and-preview)))

Note that the markdown-specific commands use the C-c C-c and C-c C-s prefixes.

Using polymode, lets add syntax coloring to Markdown code blocks similar to what we do with Org:

  (use-package polymode
    :config
    (define-hostmode poly-markdown-hostmode :mode 'markdown-mode)
    (define-auto-innermode poly-markdown-fenced-code-innermode
                           :head-matcher (cons "^[ \t]*\\(```{?[[:alpha:]].*\n\\)" 1)
                           :tail-matcher (cons "^[ \t]*\\(```\\)[ \t]*$" 1)
                           :mode-matcher (cons "```[ \t]*{?\\(?:lang *= *\\)?\\([^ \t\n;=,}]+\\)" 1)
                           :head-mode 'host
                           :tail-mode 'host)
    (define-polymode poly-markdown-mode
                     :hostmode 'poly-markdown-hostmode
                     :innermodes '(poly-markdown-fenced-code-innermode))

    :mode ((rx ".md" string-end) . poly-markdown-mode))

YAML

Doing a lot of YAML work, but this projeDoing a lot of YAML work, but this yaml-mode project needs a new maintainer, so Ive switch to yaml-pro that is now based on Tree Sitter. Lets make sure the Tree-Sitter version works:

  (if (string-search "TREE_SITTER" system-configuration-features)
      (progn
        (use-package yaml-ts-mode
          :mode ((rx ".y" (optional "a") "ml" string-end)
                 (rx (optional ".") "yamllint"))
          :hook (yaml-ts-mode . display-line-numbers-mode))

        (use-package yaml-pro
          :straight (:host github :repo "zkry/yaml-pro")
          :after yaml-ts-mode
          :hook (yaml-ts-mode . yaml-pro-ts-mode)))

    (use-package yaml-mode
      :mode (rx ".y" (optional "a") "ml" string-end)
            (rx (optional ".") "yamllint")
      :hook (yaml-mode . display-line-numbers-mode)))

This comes with a list of nice refactoring features that we can attach to the local leader:

  (when (string-search "TREE_SITTER" system-configuration-features)
    (use-package yaml-pro
      :config
      (ha-local-leader :keymaps 'yaml-pro-ts-mode-map
        "u" '("up" . yaml-pro-ts-up-level)                   ; C-c C-u
        "j" '("next" . yaml-pro-ts-next-subtree)             ; C-c C-n
        "k" '("previous" . yaml-pro-ts-prev-subtree)         ; C-c C-p

        "m" '("mark tree" . yaml-pro-ts-mark-subtree)        ; C-c C-@
        "d" '("kill subtree" . yaml-pro-kill-subtree)     ; C-c C-x C-w
        "y" '("paste tree" . yaml-pro-ts-paste-subtree)      ; C-c C-x C-y

        "'" '("edit" . yaml-pro-edit-ts-scalar)              ; C-c '

        "r"  '(:ignore t :which-key "refactor")
        "r k" '("move up" . yaml-pro-ts-move-subtree-up)     ; s-↑
        "r j" '("move down" . yaml-pro-ts-move-subtree-down) ; s-↓

        ;; "r " '("" . yaml-pro-ts-meta-return)              ; M-<return>
        "r c" '("convolute" . yaml-pro-ts-convolute-tree)    ; M-?
        "r i" '("indent" . yaml-pro-ts-indent-subtree)       ; C-c >
        "r o" '("outdent" . yaml-pro-ts-unindent-subtree)))) ; C-c <

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:

  pip install yamllint

Jinja2

A lot of projects (like Ansible and Zuul) uses Jinja2 with YAML, so we install the jinja2-mode:

  (use-package jinja2-mode
    :mode (rx ".j2" string-end))

Jinja is a templating system that integrates inside formats like JSON, HTML or YAML. The polymode project glues modes like jinja2-mode to yaml-mode.

I adapted this code from the poly-ansible project:

  (use-package polymode
    :config
    (define-hostmode poly-yaml-hostmode :mode 'yaml-ts-mode)
    (defcustom pm-inner/jinja2
      (pm-inner-chunkmode :mode #'jinja2-mode
                          :head-matcher "{[%{#][+-]?"
                          :tail-matcher "[+-]?[%}#]}"
                          :head-mode 'body
                          :tail-mode 'body
                          :head-adjust-face t)
      "Jinja2 chunk."
      :group 'innermodes
      :type 'object)
    (define-polymode poly-yaml-jinja2-mode
      :hostmode 'poly-yaml-hostmode
      :innermodes '(pm-inner/jinja2))

    :mode ((rx ".y" (optional "a") "ml" string-end) . poly-yaml-jinja2-mode))

Ansible

Do I consider all YAML files an Ansible file needing ansible-mode? Maybe we just have a toggle for when we want the Ansible feature.

  (use-package ansible
    :mode (rx (or "playbooks" "roles"))
    :config
    (setq ansible-vault-password-file "~/.ansible-vault-passfile")
    (ha-leader "t y" 'ansible))

The ansible-vault-password-file variable needs to change per project, so lets use the .dir-locals.el file, for instance:

  ((nil . ((ansible-vault-password-file . "playbooks/.vault-password"))))

The YAML files get access Ansibles documentation using the ansible-doc project:

  (use-package ansible-doc
    :hook (ansible-mode . ansible-doc-mode)
    :config
    (ha-local-leader :keymaps 'ansible-key-map
      "d"  '(:ignore t :which-key "docs")
      "d d" 'ansible-doc))

Can we integrate Ansible with LSP using ansible-language-server project (see this documentation)?

Using npm to install the program:

  npm installl -g @ansible/ansible-language-server

But … will I get some use out of this? Ill come back to it later.

Docker

Edit Dockerfiles with the dockerfile-mode project:

  (use-package dockerfile-mode
    :mode (rx string-start "Dockerfile")
    :config
    (make-local-variable 'docker-image-name)
    (defvaralias 'docker-image-name 'dockerfile-image-name nil)

    (ha-local-leader :keymaps 'dockerfile-mode-map
      "b" '("build" . dockerfile-build-buffer)
      "B" '("build no cache" . dockerfile-build-no-cache-buffer)
      "t" '("insert build tag" . ha-dockerfile-build-insert-header))

    (defun ha-dockerfile-build-insert-header (image-name)
      "Prepends the default Dockerfile image name at the top of a file."
      (interactive "sDefault image name: ")
      (save-excursion
        (goto-char (point-min))
        (insert (format "## -*- dockerfile-image-name: \"%s\" -*-" image-name))
        (newline))))

Control Docker from Emacs using the docker.el project:

  (use-package docker
    :commands docker
    :config
    (ha-leader "a d" 'docker))

Unclear whether I want to Tramp into a running container:

  (use-package docker-tramp
    :defer t
    :after docker)

Shell Scripts

While I don't like writing them, I can't get away from them. Check out the goodies in this video.

While filename extensions work fine most of the time, I don't like to pre-pend .sh to the shell scripts I write, and instead, would like to associate shell-mode with all files in a bin directory:

  (use-package sh-mode
    :straight (:type built-in)
    :mode (rx (or (seq ".sh" eol)
                  "/bin/"))
    :init
    (setq sh-basic-offset 2
          sh-indentation 2)
    :config
    (ha-auto-insert-file (rx (or (seq ".sh" eol)
                                 "/bin/"))
                         "sh-mode.sh")
    :hook
    (after-save . executable-make-buffer-file-executable-if-script-p))

Note: we make the script executable by default. See this essay for details, but it turns on the executable bit if the script has a shebang at the top of the file.

The shellcheck project integrates with /git/howard/hamacs/src/commit/264720be8ea9bde2ac8c8f40aa1708fecd9051d0/Flycheck. First, install the executable into the system, for instance, on a Mac:

  brew install shellcheck

And we can enable it:

  (flycheck-may-enable-checker 'sh-shellcheck)

Place the following on a line before a shell script warning to ignore it:

# shellcheck disable=SC2116,SC2086

See this page for details.

Integration with the Bash LSP implementation. First, install that too:

  brew install bash-language-server

Fish Shell

I think the fish shell is an interesting experiment (and I appreciate the basics that come with fish-mode).

  (use-package fish-mode
    :mode (rx ".fish" eol)
    :config
    (ha-auto-insert-file (rx ".fish") "fish-mode.sh")
    :hook
    (fish-mode . (lambda () (add-hook 'before-save-hook 'fish_indent-before-save))))