nathanvy / dotemacs

My emacs configuration

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Emacs

Here is my configuration for the almighty emacs editor. I used to think it was long and comprehensive until I saw some of the more prolific configs that abound, so I guess it might be considered somewhat minimalist but I’ve been using emacs for many years and this works for me. The vast majority of my config is just loading packages for working with various programming languages, et cetera, but there are some tweaks included herein. My philosophy is to make few changes, only when there’s a clear value-add, and so this file has been built up incrementally over the course of several years. I believe this is the best way to leverage emacs’ strengths and mold it to yourself.

I do not use `vim`-style bindings because I am not a deranged lunatic.

About this file

This is a so-called “literate” code file, where the code and its documentation are intertwined as one file and separated at compile/runtime. This file is the primary configuration artifact and most of the sausage gets made here. There is also an early-init file that sets a few variables before the package system and GUI are loaded.

Usage

  1. Install emacs. Caveat: I primarily use emacs on macOS using the Yamamoto (aka the railwaycat) fork, so your mileage may vary.
  2. Issue the following commands:
$ git clone git@github.com:nathanvy/dotemacs.git
$ cd .emacs.d/
$ make
  1. Go make an espresso while `straight` bootstraps itself and begins pulling down packages.

I toyed briefly with `elpaca` which runs many tasks in parallel and has a nifty menu, but the asynchronous nature of package loading meant I was getting weird behaviour that I have not to date had time to isolate and fix, but I’ll find time to grok it eventually, and will probably end up migrating there.

Infrastructure

First enable lexical binding, then set up straight which is a modern package manager that sidesteps the built-in package.el functionality, because package.el is a dumpster fire and apparently not even the emacs maintainers know how it works. Note that we use tree-sitter so technically we depend on emacs version 29+, but you could use the tree-sitter packages for prior versions. In that case change/delete the check below.

;; -*- lexical-binding: t; -*-

(when (version< emacs-version "29")
  (error "This emacs requires a min version of 29 but you're running %s" emacs-version))

(setq user-full-name "Nathan Van Ymeren"
      user-mail-address "n@0x85.org")

(setq use-short-answers t)
(tool-bar-mode -1)

(set-default-coding-systems 'utf-8)  
(prefer-coding-system       'utf-8)  
(set-terminal-coding-system 'utf-8)  
(set-keyboard-coding-system 'utf-8)  
(set-language-environment "English") 

(defvar bootstrap-version)
(let ((bootstrap-file
       (expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
      (bootstrap-version 6))
  (unless (file-exists-p bootstrap-file)
    (with-current-buffer
        (url-retrieve-synchronously
         "https://raw.githubusercontent.com/radian-software/straight.el/develop/install.el"
         'silent 'inhibit-cookies)
      (goto-char (point-max))
      (eval-print-last-sexp)))
  (load bootstrap-file nil 'nomessage))

(straight-use-package 'use-package)
(require 'use-package-ensure)
(setq straight-use-package-by-default t)

I rarely if ever use Customize but when I do I prefer it to put its mess into its own tidy file. Also here I’ll load some secrets that I don’t want in version control.

(setq custom-file "~/.emacs.d/custom.el")
(load custom-file)

(when (file-exists-p (expand-file-name "secrets.el" user-emacs-directory))
  (load-file (expand-file-name "secrets.el" user-emacs-directory)))

This sets up a centralized location where emacs will leave its temporary files instead of littering our filesystem.

 (setq temporary-file-directory "~/.emacs.d/saves")
 (setq backup-directory-alist
	`((".*" . ,temporary-file-directory)))
 (setq auto-save-file-name-transforms
	`((".*" ,temporary-file-directory t)))

 (message "Deleting old backup files...")
 (let ((week (* 60 60 24 7))
	(current (float-time (current-time))))
   (dolist (file (directory-files temporary-file-directory t))
     (when (and (backup-file-name-p file)
		 (> (- current (float-time (nth 5 (file-attributes file))))
		    week))
	(message "%s" file)
	(delete-file file))))

Utilities

Some utility functions:

(defun increment-number-at-point ()
  (interactive)
  (skip-chars-backward "0-9")
  (or (looking-at "[0-9]+")
      (error "No number at point"))
  (replace-match (number-to-string (1+ (string-to-number (match-string 0))))))

(defun decrement-number-at-point ()
  (interactive)
  (skip-chars-backward "0-9")
  (or (looking-at "[0-9]+")
      (error "No number at point"))
  (replace-match (number-to-string (1- (string-to-number (match-string 0))))))

(defun insert-line-below ()
  "Insert a blank line below point"
  (interactive)
  (move-beginning-of-line nil)
  (insert "\n")
  (if electric-indent-inhibit
      (let* ((indent-end (progn (crux-move-to-mode-line-start) (point)))
             (indent-start (progn (move-beginning-of-line nil) (point)))
             (indent-chars (buffer-substring indent-start indent-end)))
        (forward-line -1)
        (insert indent-chars))
    (forward-line -1)
    (indent-according-to-mode)))

Packages

Now that we’re bootstrapped we can start pulling in stuff that we use to get other stuff done. We’ll start with some OS-specific stuff:

(when (eq system-type 'darwin)
  (customize-set-variable 'native-comp-driver-options '("-Wl,-w")) ;;revisit in emacs 29
  (use-package exec-path-from-shell
    :config
    (exec-path-from-shell-initialize)))
;;  (when (eq system-type 'gnu/linux))

And some general utility packages. Transpose-frame lets us move frames around easily, and smex aka Smart M-x is just groovy.

(use-package transpose-frame)
(use-package smex)
(use-package projectile)
(use-package magit)
(use-package which-key
  :config
  (which-key-mode))

There are lots of competing (or perhaps it would be better to say overlapping) packages in this space but I like good old ido. It does what I need. ido is built in but if you actually set `ido-everywhere = 1` you may discover it’s not actually everywhere so we add ido-completing-read+

(setq ido-enable-flex-matching t)
(ido-mode 1)
(ido-everywhere 1)
(use-package ido-completing-read+
  :config
  (ido-ubiquitous-mode 1))

Visuals

I stumbled upon prism-mode by accident after much mucking about with rainbow-delimiters and friends, and I’ve really come to prefer prism for coloring.

I shopped around for themes quite a bit because emacs by default is quite frankly hideous, and I spent quite some time embracing the glorious 80s aesthetic and for a while enjoyed a super dank synthwave type theme. Originally I had settled on the vscode-dark+ theme which I really liked and heartily recommend but sometimes you want to have more fun. Base16-based themes also get an honorable mention for being good. Lots of folks use solarized but I found it didn’t have enough contrast for me. These days I appear to have settled on nord.

We thank these themes for their prior service:

  • synthwave-emacs
  • doom-outrun-electric
  • doom-laserwave
  • tomorrow-night
  • vscode-dark
(column-number-mode t)
(show-paren-mode t)
(setq-default indent-tabs-mode nil)

(use-package nord-theme
  :if (display-graphic-p)
  :straight (nord-theme
	       :type git
	       :host github
	       :repo "nordtheme/emacs")
  :config
  (set-face-attribute 'default nil :family "Monaco")
  (set-face-attribute 'fixed-pitch nil :family "Monaco")
  (set-face-attribute 'variable-pitch nil :family "SF Pro Display" :height 140)
  (load-theme 'nord t))

(use-package all-the-icons
  :if (display-graphic-p))

(use-package mode-line-bell
  :config (mode-line-bell-mode))

;; temporarily disabled
;; (use-package prism
;;     :commands prism-mode
;;     :init
;;     (add-hook 'go-mode-hook #'prism-mode)
;;     (add-hook 'csharp-mode-hook #'prism-mode)
;;     (add-hook 'js-mode-hook #'prism-mode)
;;     (add-hook 'js-jsx-mode-hook #'prism-mode)
;;     (add-hook 'typescirpt-mode-hook #'prism-mode)
;;     (add-hook 'c++-mode-hook #'prism-mode)
;;     (add-hook 'emacs-lisp-mode-hook #'prism-mode)
;;     (add-hook 'ielm-mode-hook #'prism-mode)
;;     (add-hook 'lisp-mode-hook #'prism-mode)
;;     (add-hook 'lisp-interaction-mode-hook #'prism-mode)
;;     (add-hook 'scheme-mode-hook #'prism-mode)
;;     (add-hook 'python-mode-hook #'prism-whitespace-mode))

Parrot Mode needs no introduction, and no explanation.

(use-package parrot
  :if (display-graphic-p)
  :config (parrot-mode))

Language Server Protocol

Emacs and LSP together make for a fantastic editing experience and has deprecated a lot of previously-indispensable stuff so we’ll get it going along with company for completion. For pre-29 emacs this is where I also use-package‘d the tree-sitter packages and languages but with the release of 29.1 that’s no longer necessary as long as you compile emacs --with-tree-sitter

(use-package lsp-mode
  :init
  ;; set prefix for lsp-command-keymap (few alternatives - "C-l", "C-c l")
  (setf lsp-keymap-prefix "C-c l")
  :hook ((go-ts-mode . (lambda ()
			   (lsp-go-install-save-hooks)
			   (lsp)))
	   (csharp-ts-mode . lsp)
	   (ess-r-mode . lsp)
	   (web-mode . lsp)
	   (js-ts-mode .lsp)
	   (js-jsx-mode . lsp)
	   (typescript-ts-mode . lsp)
	   (c-or-c++-ts-mode . lsp)
	   (python-ts-mode . (lambda ()
			       (require 'lsp-python-ms)
			       (lsp))))
  :commands lsp lsp-deferred
  :config
  (setq lsp-log-io nil))

(use-package lsp-ui
  :hook (lsp-mode . lsp-ui-mode))

(use-package flycheck
  :init (global-flycheck-mode))

(use-package lsp-treemacs
  :commands lsp-treemacs-errors-list)

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

Thanks Mickey Petersen for this list, to which I’ve added a few:

;; https://www.masteringemacs.org/article/how-to-get-started-tree-sitter
(setq treesit-language-source-alist
 '((bash "https://github.com/tree-sitter/tree-sitter-bash")
   (cmake "https://github.com/uyha/tree-sitter-cmake")
   (css "https://github.com/tree-sitter/tree-sitter-css")
   (csharp "https://github.com/tree-sitter/tree-sitter-c-sharp")
   (lisp "https://github.com/theHamsta/tree-sitter-commonlisp")
   (cuda "https://github.com/theHamsta/tree-sitter-cuda")
   (elisp "https://github.com/Wilfred/tree-sitter-elisp")
   (fortran "https://github.com/stadelmanma/tree-sitter-fortran")
   (go "https://github.com/tree-sitter/tree-sitter-go")
   (html "https://github.com/tree-sitter/tree-sitter-html")
   (java "https://github.com/tree-sitter/tree-sitter-java")
   (javascript "https://github.com/tree-sitter/tree-sitter-javascript" "master" "src")
   (julia "https://github.com/tree-sitter/tree-sitter-julia")
   (json "https://github.com/tree-sitter/tree-sitter-json")
   (latex "https://github.com/latex-lsp/tree-sitter-latex")
   (lua "https://github.com/Azganoth/tree-sitter-lua")
   (make "https://github.com/alemuller/tree-sitter-make")
   (markdown "https://github.com/ikatyang/tree-sitter-markdown")
   (objc "https://github.com/jiyee/tree-sitter-objc")
   (org "https://github.com/milisims/tree-sitter-org")
   (perl "https://github.com/tree-sitter-perl/tree-sitter-perl")
   (php "https://github.com/tree-sitter/tree-sitter-php")
   (proto "https://github.com/mitchellh/tree-sitter-proto")
   (python "https://github.com/tree-sitter/tree-sitter-python")
   (R "https://github.com/r-lib/tree-sitter-r")
   (ruby "https://github.com/tree-sitter/tree-sitter-ruby")
   (rust "https://github.com/tree-sitter/tree-sitter-rust")
   (scheme "https://github.com/6cdh/tree-sitter-scheme")
   (sql "https://github.com/m-novikov/tree-sitter-sql")
   (toml "https://github.com/tree-sitter/tree-sitter-toml")
   (tsx "https://github.com/tree-sitter/tree-sitter-typescript" "master" "tsx/src")
   (typescript "https://github.com/tree-sitter/tree-sitter-typescript" "master" "typescript/src")
   (yaml "https://github.com/ikatyang/tree-sitter-yaml")))

But we don’t want to manually deal with enabling the <name>-ts-mode and wondering which are available so we’ll just use treesit-auto:

     ;;https://github.com/renzmann/treesit-auto
     (use-package treesit-auto
	:config
	(setq treesit-auto-install 'prompt)
	(global-treesit-auto-mode))

Snippets

In 2021 I started writing a lot of Go (golang) and there’s an awful lot of repetitive error checking when trying to follow the idiomatic style. I got annoyed at writing the same if construct hundreds of times so I decided it was finally time to install yasnippet. It comes with TAB bound to yas-expand by default which I don’t like, so I disabled it here by setting it to nil, and moved it to a different key combination at the end of this file.

(use-package yasnippet
  :init
  (yas-global-mode)
  (define-key yas-minor-mode-map (kbd "<tab>") nil)
  (define-key yas-minor-mode-map (kbd "TAB") nil))

Lisp

I hated lisp at first but I’ve found that it’s really grown on me. It has its warts but all languages do. We don’t leverage LSP here since most lisp implementations predate Language Servers and provide their own analogous constructs that are more tightly integrated with the REPL anyway. Sly is a fork of SLIME and is more actively developed.

(use-package sly
  :config
  (setq inferior-lisp-program "sbcl")
  (setq org-babel-lisp-eval-fn #'sly-eval)
  (setq org-confirm-babel-evaluate nil))

(use-package paredit
  :mode "paredit-mode"
  :commands enable-paredit-mode
  :init
  (add-hook 'emacs-lisp-mode-hook #'enable-paredit-mode)
  (add-hook 'eval-expression-minibuffer-setup-hook #'enable-paredit-mode)
  (add-hook 'ielm-mode-hook #'enable-paredit-mode)
  (add-hook 'lisp-mode-hook #'enable-paredit-mode)
  (add-hook 'lisp-interaction-mode-hook #'enable-paredit-mode)
  (add-hook 'scheme-mode-hook #'enable-paredit-mode))

R

At the time of writing this paragraph I’m in an MBA program and for our analytics courses they inexplicably chose R over Python, because I guess they hate us. So here’s ess (Emacs Speaks Statistics). I haven’t bothered to set up polymode for doing “RMarkdown” shenanigans because org is good enough for me.

     (use-package ess
	:bind (:map ess-r-mode-map
		    ("M-p" . " %>%"))
	:config
	(require 'ess-r-mode))

In the course of writing assignments I ran into a problem where certain tidyverse packages were causing weird coloration in the inferior ESS R buffer, such that the text was basically unreadable on a dark background. After some digging it seems that the R process emits super leet haxors ANSI color codes, because you know why not?

The issue is this one: emacs-ess/ESS#1193

And the solution/workaround was:

(defun my-inferior-ess-init ()
  "Workaround for https://github.com/emacs-ess/ESS/issues/1193"
  (add-hook 'comint-preoutput-filter-functions #'xterm-color-filter -90 t)
  (setq-local ansi-color-for-comint-mode nil))

(use-package xterm-color
    :straight (xterm-color
		 :type git
		 :host github
		 :repo "atomontage/xterm-color")
    :config
    (add-hook 'inferior-ess-mode-hook #'my-inferior-ess-init))

Other programming languages

Most of these are simple invocations of use-package and require no explanation.

	(use-package web-mode)

	(use-package glsl-mode
	  :straight (glsl-mode
		     :type git
		     :host github
		     :repo "jimhourihan/glsl-mode"))

	(use-package python)
	(use-package lsp-python-ms
	  :after (lsp-mode python)
	  :init (setq lsp-python-ms-auto-install-server t))

	(defun lsp-go-install-save-hooks ()
	  (add-hook 'before-save-hook #'lsp-format-buffer t t)
	  (add-hook 'before-save-hook #'lsp-organize-imports t t))
	(use-package go-mode)

Some generally-useful stuff like Dashboard and packages like Org for writing prose comes here. If you read below and are confused, it’s not a typo: pdflatex needs to be invoked three times because of the way the standard LaTeX recipe works, which goes something like this:

  1. $ latex <filename>
  2. $ bibtex <filename>
  3. $ latex <filename>
  4. $ latex <filename>

Basically, the first time you run it, latex writes citations and stuff like \label to a .aux file, which is what bibtex reads. BibTeX reads that file as well as the .bib file and uses that to format the references. When you run LaTeX a second time it also reads both .aux and .tex files and if bibtex generated any .bbl files it reads those as well, which is how it inserts the references into the output. The third run is what causes the citations and labels to get inserted into the output. If you have multiple bibliographies you’ll need more invocations of bibtex and latex and it quickly becomes a clusterfuck. Anyways this is why I have three calls to pdflatex in there.

(use-package dashboard
  :config
  (dashboard-setup-startup-hook)
  (setq dashboard-items '((recents . 20) (bookmarks . 20)))
  (setq dashboard-banner-logo-title "Hacks and glory await!")
  (setq recentf-exclude '("bookmarks"))
  (setq dashboard-startup-banner "~/.emacs.d/dashboard-logo.png"))

(use-package org
  :init
  (setf org-list-allow-alphabetical t)
  (setf org-src-tab-acts-natively t)
  (setf org-startup-truncated nil)
  :config
  (org-babel-do-load-languages 'org-babel-load-languages '((R . t)
							     (lisp . t)
							     (emacs-lisp . t)))
  (set-face-attribute 'org-table nil :inherit 'fixed-pitch)
  (set-face-attribute 'org-code nil :inherit 'fixed-pitch)
  (set-face-attribute 'org-block nil :inherit 'fixed-pitch)
  (set-face-attribute 'org-block-begin-line nil :inherit 'fixed-pitch)
  (set-face-attribute 'org-block-end-line nil :inherit 'fixed-pitch)
  (set-face-attribute 'org-block-begin-line nil :slant 'normal :underline nil :extend nil)
  (set-face-attribute 'org-block-end-line nil :slant 'normal :overline nil :extend nil)
  (setf org-html-preamble nil)
  (setf org-html-postamble nil)
  (setq org-latex-listings 'minted)
  (setq org-latex-packages-alist '(("" "minted")))
  (setq org-latex-pdf-process
  '("pdflatex -shell-escape -interaction nonstopmode -output-directory %o %f"
    "pdflatex -shell-escape -interaction nonstopmode -output-directory %o %f"
    "pdflatex -shell-escape -interaction nonstopmode -output-directory %o %f")))

(use-package org-bullets
  :init
  (add-hook 'org-mode-hook (lambda ()
			       (org-bullets-mode 1))))

(use-package ox-rfc)

(use-package markdown-mode
  :commands (markdown-mode gfm-mode)
  :mode (("README\\.md\\'" . gfm-mode)
	   ("\\.md\\'" . markdown-mode)
	   ("\\.markdown\\'" . markdown-mode))
  :init (setq markdown-command "multimarkdown"))

For writing prose or anything non-code I like to use Olivetti which adds some nice gutters on either side of the screen and pair it with variable pitch fonts.

(use-package olivetti
  :init
  (add-hook 'text-mode-hook (lambda ()
				(olivetti-mode 1)
				(olivetti-set-width 140)
				(variable-pitch-mode 1))))

Keybinds

I put package-local/namespaced binds with their use-package declarations but I decided to collect all my global custom keybinds into one section here at the end to keep better tabs on my C-c <single char> namespace.

(global-set-key (kbd "C-c d") 'lsp-find-definition)
(global-set-key (kbd "C-c g") 'rgrep)

(global-set-key (kbd "C-c i") 'flip-frame)
(global-set-key (kbd "C-c o") 'flop-frame)
(global-set-key (kbd "C-c r") 'rotate-frame-clockwise)
(global-set-key (kbd "C-c t") 'treemacs)

(global-set-key (kbd "C-c y") 'yas-expand)

(global-set-key (kbd "C-c n") 'parrot-rotate-next-word-at-point)
(global-set-key (kbd "C-c p") 'parrot-rotate-prev-word-at-point)

(global-set-key (kbd "C-c q") 'query-replace)
(global-set-key (kbd "C-c x") 'query-replace-regexp)

(global-set-key (kbd "M-x") 'smex)
(global-set-key (kbd "M-X") 'smex-major-mode-commands)

(global-set-key (kbd "M-o") 'insert-line-below)

;; This is the old M-x.
(global-set-key (kbd "C-c C-c M-x") 'execute-extended-command)

(global-set-key (kbd "C-c +") 'increment-number-at-point)
(global-set-key (kbd "C-c -") 'decrement-number-at-point)

(global-unset-key (kbd "M-t"))

About

My emacs configuration


Languages

Language:Emacs Lisp 78.2%Language:Makefile 21.8%