hamacs/ha-programming.org
2022-03-03 15:20:27 -08:00

24 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. However…

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.

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 only checks for misspellings in comments. Useful!

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

Flymake

Grab the latest version of Flymake in order to integrate with LSP. While we are at it, lets add some keybindings.

  (use-package flymake
    :config
    (ha-prog-leader
      ">" '("next problem" . flymake-goto-next-error)
      "<" '("previous problem" . flymake-goto-prev-error)
      "p" '(:ignore t :which-key "problems")
      "p s" '("start checking" . flymake-start)
      "p t" '("toggle flymake" . flymake-mode)
      "p p" '("show problem" . flymake-show-diagnostic)
      "p P" '("list all problems" . flymake-show-buffer-diagnostics)
      "p l" '("show log buffer" . flymake-switch-to-log-buffer)
      "p b" '("show log buffer" . flymake-running-backends)))

The question is why not use the newer flycheck? Speed used to be the advantage, however, Im now pushing this stuff to LSP, so speed is less of an issue. However, what about when I am not using LSP?

  (use-package flycheck
    :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-prog-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 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

Ive used the Dash API Documentation browser (an external application) with Emacs, however, this is only available for Mac.

  (use-package dash-at-point
    :commands (dash-at-point)
    :config
    (define-key evil-normal-state-map (kbd "g D") 'dash-at-point))

However, Im interested in using devdocs instead, which is similar, but keeps it all inside Emacs (and works on my Linux system). There are seems to be two competing Emacs projects for this.

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
    :after evil
    :config
    (define-key evil-normal-state-map (kbd "g D") 'devdocs-lookup)

    (ha-prog-leader
      "d"  '(:ignore t :which-key "docs")
      "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
    :config
    (define-key evil-normal-state-map (kbd "g D") 'devdocs-browser-open)

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

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

Instead, we install LSP Mode (and friends), which simplifies my configuration:

  (use-package lsp-mode
    :commands lsp
    :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")
    :hook ((lsp-mode . lsp-enable-which-key-integration)))

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 this page for the default keybindings.

UI

The lsp-ui project offers much of the display and interface to LSP:

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

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-prog-leader
        "g"  '(:ignore t :which-key "goto")
        "g m" '("imenu" . lsp-ui-imenu))
      (add-hook 'lsp-after-open-hook 'lsp-enable-imenu))

Treemacs

The lsp-treemacs offers a project-specific structure oriented to the code:

  (use-package lsp-treemacs
    :config
    (ha-prog-leader
          "0" '("treemacs" . lsp-treemacs-symbols)))

Debugging

Do we want to use a debugger?

  (use-package dap-mode
     :init
     (require 'dap-python))

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

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" . eval-print-last-sexp))

Ligatures

The idea of using math symbols for a programming languages keywords is cute, but confusing when working with other people, and they are looking at my screen:

  (set-ligatures! 'python-mode nil)

The rest of the ligature system in Doom is nice.

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:

(setq doom-modeline-lsp t)
(setq doom-modeline-env-version t)

Languages

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

YAML and Jinja

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

(use-package yaml-mode
  :mode "\\.ya?ml\\'")

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

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

Emacs Lisp

Why yes, I do find I code a lot in Emacs…

  (ha-auto-insert-file (rx ".el" eol) "emacs-lisp-mode.el")

However, most of my Emacs Lisp code is in literate org files.

Clever Parenthesis

The evil-cleverparens solves having me create keybindings to the smartparens project by updating the evil states with Lisp-specific bindings.

  (use-package evil-cleverparens
    :after smartparens-mode
    :custom
    evil-cleverparens-use-additional-bindings t
    evil-cleverparens-use-additional-movement-keys t
    evil-cleverparens-use-s-and-S nil ; using evil-sniper

    :init
    (require 'evil-cleverparens-text-objects)

    :hook (prog-mode . evil-cleverparens-mode))  ;; All the languages!
    ;; Otherwise: (emacs-lisp-mode . evil-cleverparens-mode)

I would like to have a list of what keybindings do what:

  • M-h / M-l move back/forward by functions
  • H / L move back/forward by s-expression
  • M-i insert at the beginning of the form
  • M-a appends at the end of the form
  • M-o new form after the current sexp
  • M-O new form before the current sexp
  • M-j / M-k drags thing at point and back and forth in the form
  • > slurp forward if at the end of form, at beginning, it barfs backwards
  • < slurp backward if at start of form, at the end, it barfs forwards
  • M-( / M-) wraps next/previous form in parens (braces and brackets work too)
  • x unwraps if the point is on the ( of an expression.
  • D deletes an entire s-expression, but this can depend on the position of the point.

The other advantage is moving around by s-expressions. This takes a little getting used to, for instance:

  • [ and ] move from paren to paren, essentially, from s-expression.
  • H and L act similarly to the above.
  • ( and ) move up to the parent s-expression

Other nifty keybindings that I need to commit to muscle memory include:

M-q sp-indent-defun
M-J sp-join-sexp
M-s sp-splice-sexp
M-S sp-split-sexp
M-t sp-transpose-sexp
M-v sp-convolute-sexp
M-r sp-raise-sexp
Eval Current Expression

A feature I enjoyed from Spacemacs is the ability to evaluate the s-expression currently containing the point. Not sure how how they made it, but cleverparens can help:

(defun ha-eval-current-expression ()
  "Evaluates the expression the point is currently 'in'.
It does this, by jumping to the end of the current
expression (using evil-cleverparens), and evaluating what it
finds at that point."
  (interactive)
  (save-excursion
    (evil-cp-next-closing)
    (evil-cp-forward-sexp)
    (call-interactively 'eval-last-sexp)))

And we just need to bind it. The following is Doom-specific:

  (ha-prog-leader
    "e c" '("current" . ha-eval-current-expression))

Dim those Parenthesis

The paren-face project lowers the color level of parenthesis which I personally find better.

(use-package paren-face
  :hook (emacs-lisp-mode . paren-face-mode))

I'm going to play with the parinfer package.

Lispy

Sacha had an interesting idea to generate a Hydra from a mode map:

key function column
< lispy-barf
A lispy-beginning-of-defun
j lispy-down
Z lispy-edebug-stop
B lispy-ediff-regions
G lispy-goto-local
h lispy-left
N lispy-narrow
y lispy-occur
o lispy-other-mode
J lispy-outline-next
K lispy-outline-prev
P lispy-paste
l lispy-right
I lispy-shifttab
> lispy-slurp
SPC lispy-space
xB lispy-store-region-and-buffer
u lispy-undo
k lispy-up
v lispy-view
V lispy-visit
W lispy-widen
D pop-tag-mark
x see
L unbound
U unbound
X unbound
Y unbound
H lispy-ace-symbol-replace Edit
c lispy-clone Edit
C lispy-convolute Edit
n lispy-new-copy Edit
O lispy-oneline Edit
r lispy-raise Edit
R lispy-raise-some Edit
\ lispy-splice Edit
S lispy-stringify Edit
i lispy-tab Edit
xj lispy-debug-step-in Eval
xe lispy-edebug Eval
xT lispy-ert Eval
e lispy-eval Eval
E lispy-eval-and-insert Eval
xr lispy-eval-and-replace Eval
p lispy-eval-other-window Eval
q lispy-ace-paren Move
z lispy-knight Move
s lispy-move-down Move
w lispy-move-up Move
t lispy-teleport Move
Q lispy-ace-char Nav
- lispy-ace-subword Nav
a lispy-ace-symbol Nav
b lispy-back Nav
d lispy-different Nav
f lispy-flow Nav
F lispy-follow Nav
g lispy-goto Nav
xb lispy-bind-variable Refactor
xf lispy-flatten Refactor
xc lispy-to-cond Refactor
xd lispy-to-defun Refactor
xi lispy-to-ifs Refactor
xl lispy-to-lambda Refactor
xu lispy-unbind-variable Refactor
M lispy-multiline Other
xh lispy-describe Other
m lispy-mark-list Other
(defvar my-lispy-bindings bindings)

(defvar ha-hydra-lispy-bindings
  (cl-loop for x in my-lispy-bindings
           unless (string= "" (elt x 2))
           collect
           (list (car x)
                 (intern (elt x 1))
                 (when (string-match "lispy-\\(?:eval-\\)?\\(.+\\)"
                                     (elt x 1))
                   (match-string 1 (elt x 1)))
                 :column
                 (elt x 2)))
  "Collection of memorable Lispy functions")

(eval
 `(defhydra
    ,(append '(("<f14>" nil :exit t)) ha-hydra-lispy-bindings )

   ))
(funcall defhydra
         `(my/lispy-cheat-sheet (:hint nil :foreign-keys run)
                                ))
(with-eval-after-load "lispy"
  (define-key lispy-mode-map (kbd "<f14>") 'my/lispy-cheat-sheet/body))

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