hamacs/ha-programming.org
2022-08-09 21:33:21 -07:00

18 KiB
Raw Blame History

General Programming Configuration

A literate programming file for helping me program.

Introduction

Seems that all programming interfaces and workflows behave similarly. One other helper routine is a general macro for org-mode files:

  (general-create-definer ha-prog-leader
      :states '(normal visual motion)
      :keymaps 'prog-mode-map
      :prefix "SPC m"
      :global-prefix "<f17>"
      :non-normal-prefix "S-SPC")

General

The following work for all programming languages.

Markdown

All the READMEs and other documentation use markdown-mode.

  (use-package markdown-mode
    :mode ("README\\.md\\'" . 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)))

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

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
    (flymake-mode -1)
    (global-flycheck-mode)
    (ha-leader "t c" 'flycheck-mode)

    (ha-leader
      ">" '("next problem" . flycheck-next-error)
      "<" '("previous problem" . flycheck-previous-error)

      "P" '(:ignore t :which-key "problems")
      "P b" '("error buffer" . flycheck-buffer)
      "P c" '("clear" . flycheck-clear)
      "P n" '("next" . flycheck-next-error)
      "P N" '("next" . flycheck-next-error)
      "P p" '("previous" . flycheck-previous-error)
      "P P" '("previous" . flycheck-previous-error)
      "P l" '("list all" . flycheck-list-errors)
      "P y" '("copy errors" . flycheck-copy-errors-as-kill)
      "P s" '("select checker" . flycheck-select-checker)
      "P ?" '("describe checker" . flycheck-describe-checker)
      "P h" '("display error" . flycheck-display-error-at-point)
      "P e" '("explain error" . flycheck-explain-error-at-point)
      "P H" '("help" . display-local-help)
      "P i" '("manual" . flycheck-manual)
      "P V" '("version" . flycheck-version)
      "P v" '("verify-setup" . flycheck-verify-setup)
      "P x" '("disable-checker" . flycheck-disable-checker)
      "P 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-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)))

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

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.

Language Server Protocol (LSP) Integration

The LSP is a way to connect editors (like Emacs) to languages (like Lisp)… wait, no, it was originally designed for VS Code and probably Python, but we now 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/c609e124f3a3e7aa163cd1c1c1657fc3367c9fc3/LSP%20Mode, but since I dont have heavy IDE requirements, I am finding that /git/howard/hamacs/src/commit/c609e124f3a3e7aa163cd1c1c1657fc3367c9fc3/eglot to be simpler.

eglot

The eglot package usually connects to Emacs standard command interface, so the eglot-specific code is mostly in controlling the backend servers. That said, it has a couple of eglot- commands that I want easy access to:

  (use-package eglot
    :config
    (ha-prog-leader
      "w"  '(:ignore t :which-key "eglot")
      "ws" '("start" . eglot)
      "wr" '("restart" . eglot-reconnect)
      "wb" '("events" . eglot-events-buffer)
      "we" '("errors" . eglot-stderr-buffer)
      "wq" '("quit" . eglot-shutdown)
      "wQ" '("quit all" . eglot-shutdown-all)

      "r" '("rename" . eglot-rename)
      "=" '("format" . eglot-format)
      "a" '("code actions" . eglot-code-actions)
      "i" '("imports" . eglot-code-action-organize-imports)))

This requires the company completion backend:

  (use-package company
    :after eglot
    :hook (after-init . global-company-mode)
    :bind ("s-." . company-complete))

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")))
    :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)))

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)
  (interactive "r")
  (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-prog-leader "c" '("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.
  The results are inserted 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-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))

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)

Eventually, I want to 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.

Task Runner

I've replaced my home-grown compilation list code with a more versatile Taskrunner project.

(setq ivy-taskrunner-notifications-on t
      ivy-taskrunner-doit-bin-path "/usr/local/bin/doit")

Doom provides basic support, but we need more keybindings:

(map! :leader :prefix "p"
      :desc "Project tasks" "Z" 'ivy-taskrunner
      :desc "Reun last task" "z" 'ivy-taskrunner-rerun-last-command)

While my company is typically using Rakefile and Makefile in the top-level project, I want to have my personal tasks set per-project as well. For that, I thought about using doit, where I would just create a dodo.py file that contains:

 def hello():
     """This command greets you."""
     return {
         'actions': [ 'echo hello' ],
     }

Display Configuration

Using the Doom Modeline to add notifications:

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

Languages

Simple to configure languages go here. More advanced stuff will go in their own files… eventually.

Ansible

Doing a lot of YAML work, but this project needs a new maintainer.

(use-package yaml-mode
  :mode (rx ".y" (optional "a") "ml" string-end))

Ansible uses Jinja, so we install the jinja2-mode:

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

Do I consider all YAML files an Ansible file needing ansible-mode?

  (use-package ansible
    :init
    (setq ansible-vault-password-file "~/.ansible-vault-passfile")
    ;; :hook (yaml-mode . ansible-mode)
    :config
    (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"))))

However, lets have all YAML files able to access Ansibles documentation using the ansible-doc project:

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

The poly-ansible project uses polymode, gluing jinja2-mode into yaml-mode.

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

Shell Scripts

While I don't like writing them, I can't get away from them.

While filename extensions work fine most of the time, I don't like to pre-pend .sh to the few 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/"))
    :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 appears that the executable bit is only turned on if the script has a shebang at the top of the file.

Fish Shell

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