My Emacs Setup

Posted: May 06, 2020


I've used Emacs as my primary development tool for several years. It's an incredible piece of software that has stood the test of time, and is the first thing I set up on a new computer. I wanted to share how I set it up for my personal and professional use.

While I probably use hundreds of commands and shortcuts in a given week of development and writing, I'm only sharing my basic setup and configuration here. I encourage everyone to "know your tools" and look critically at things that aren't smooth in your own workflows. Maybe there's a emacs function for that!

Emacs itself

I generally run the latest stable release (26.3 at the time of writing), compiled from source with GTK3 support. I often can't get the latest version on a stable Linux distro, and running the latest version gives the best chance of packages working correctly together and bug-free.

Daemon

If you're starting Emacs from scratch at each invocation, you're gonna have a bad time. Emacs supports --daemon, which keeps buffers and packages loaded even if the "frame" (graphical or terminal front end) client is closed.

I have the following in my ~/.initrc:

emacs --daemon

a hotkey in my window manager opens a client, or falls back to opening a full (slow) emacs editor if the daemon isn't running:

emacsclient --create-frame --alternate-editor emacs

and a shell alias in my ~/.bashrc that opens a client in non-graphical mode:

alias e='emacsclient -nw'

Terminal support

All of my configuration works just as well over a TTY as a graphical workstation. So I can bring my comfortable setup with me over ssh or mosh just by copying my ~/.emacs.el and running emacs -nw (no-window).

I also add the following to my emacs.el to add mouse click and scroll support in terminals (mostly so I'm not surprised by dissonance, rather than due to need):

;; enable mouse in terminals
(use-package xt-mouse
  :config (xterm-mouse-mode t))

;; enable mouse scrolling in terminal
(defvar alternating-scroll-down-next t)
(defvar alternating-scroll-up-next t)

(defun alternating-scroll-down-line ()
  (interactive "@")
  (when alternating-scroll-down-next
    (scroll-down-line))
  (setq alternating-scroll-down-next (not alternating-scroll-down-next)))

(defun alternating-scroll-up-line ()
  (interactive "@")
  (when alternating-scroll-up-next
    (scroll-up-line))
  (setq alternating-scroll-up-next (not alternating-scroll-up-next)))

(when (not (display-graphic-p))
  (global-set-key (kbd "<mouse-4>") 'alternating-scroll-down-line)
  (global-set-key (kbd "<mouse-5>") 'alternating-scroll-up-line))

Use can use (display-graphic-p) to detect a graphical Frame vs tty.

emacs.el config file

use-package

I use use-package to manage package installation and configuration. This also means my emacs.el can be dropped into any system with a recent Emacs and I get nearly my full setup.

;; setup package archives
(require 'package)
(add-to-list 'package-archives
             '("melpa" . "https://melpa.org/packages/") t)
(package-initialize)

;; manually install use-package package manager
(unless (package-installed-p 'use-package)
  (package-refresh-contents)
  (package-install 'use-package))

;; configure use-package

;; always download and install use-package packages
(setq use-package-always-ensure t)
;; prefer latest, not stable
(setq use-package-always-pin "melpa")
(eval-when-compile
  (require 'use-package))

;; core packages
(use-package cl)
(require 'bind-key)
;; allow minor modes to be hidden on mode bar
(use-package diminish)

;; automatically update packages weekly
(use-package auto-package-update
  :config
  (setq auto-package-update-delete-old-versions t)
  (setq auto-package-update-hide-results t)
  ;; update unprompted but not at startup to prevent login hangs from daemon
  ;; startup blocking on networked updates
  (auto-package-update-at-time "13:00"))

I'll note I never use "customizations." I maintain all my configuration in my init file itself.

Evil Vim emulation

Emacs is a great operating system, lacking only a decent editor.

I love Vim's modal interface, so I use Evil to get a Vim editor in Emacs buffers.

;; evil-leader: fast \-prefixed shortcuts
;; must come before (use-package evil)
(use-package evil-leader
  :config
  (global-evil-leader-mode)
  (evil-leader/set-key "f" 'indent-according-to-mode)
  (evil-leader/set-key "F" 'indent-region))

;; evil: vi emulation. I'm that kind of heathen.
(use-package evil
  :config
  (evil-mode 1)
  ;; evilnc: fast code comment/uncomment
  (use-package evil-nerd-commenter
    :config
    :bind (("M-;" . evilnc-comment-or-uncomment-lines)
           :map evil-normal-state-map
           ("/" . swiper))))

(evil-leader/set-key
  "\\" 'evilnc-comment-or-uncomment-lines  ;; single \
  "ci" 'evilnc-comment-or-uncomment-lines
  "cc" 'evilnc-copy-and-comment-lines
  "cp" 'evilnc-comment-or-uncomment-paragraphs
  "cr" 'comment-or-uncomment-region)

Files, projects, and search

I use Ivy (and its Swiper and Consul packages) for minibuffer completion of elisp functions, file search, etc.

I use ripgrep for text search across files/projects.

;; ivy minibuffer completion
(use-package ivy
  :diminish (ivy-mode . "")
  :init
  (setq ivy-use-virtual-buffers t)
  (setq ivy-count-format "(%d/%d) ")
  :config
  (ivy-mode 1))
;; swiper: ivy package for in-file text/regex searching
(use-package swiper)
;; counsel: ivy package for file searching
(use-package counsel
  :init
  :bind (("M-x" . counsel-M-x)
         ("C-x C-f" . counsel-find-file)
         ("C-c /" . counsel-rg)  ;; search with ripgrep
         ("C-c l" . counsel-locate)))

I use Projectile for project/workspace tasks (jump to file in project, build/test commands, and search/replace across all files in a project).

I use Bazel for work and personal projects, so I define a custom Bazel project type so Projectile will recognize it. I add a .projectile file to one-off folders like my Notes folder so I can easily jump to it.

;; projectile: project interaction
(use-package projectile
  :init (setq projectile-completion-system 'ivy)
  :config
  (projectile-mode +1)
  (define-key projectile-mode-map (kbd "C-c p") 'projectile-command-map)
  ;; Add "bazel" project type
  (projectile-register-project-type
   'bazel
   '("WORKSPACE")  ;; identifier file for a bazel project type
   :compile "bazel build "
   :test "bazel test "
   :run "bazel run "
   :test-prefix "test_"
   :test-suffix "_test"))

Magit for git

Magit is the best git front end I've ever used. I usually prefer command line tools, but Magit makes complex git workflows like interactive rebasing so much easier it's actually life-changing.

I use a couple additional helper packages to make command-line initiated workflows play nicely with Emacs as well.

;; git helper packages

;; invoke emacs from command line with temp files
(use-package with-editor)
;; use emacs to author git commit messages
(use-package git-commit)
;; super-powered git interface
(use-package magit
  :config
  (setq magit-completing-read-function 'ivy-completing-read)
  :bind ("C-x g" . magit-status))
(evil-leader/set-key "g" 'magit-status)  ;; also open with /-g

In-buffer linting

I use flycheck to automatically lint/highlight errors in source buffers. Flycheck automatically detects locally-installed tools that can be used to lint different buffer modes:

;; flycheck syntax checking
;; Python - flake8
;; C++ - clang/gcc/cppcheck
;; Rust - cargo
;; JavaScript - eslint
;; LaTeX - chktex
;; SQL - sqlint
;; Shell - shellcheck
;; Protobuf - protoc
(use-package flycheck
  :config
  (global-flycheck-mode)
  ;; (use-package flycheck-irony)
  ;; (add-hook 'flycheck-mode-hook 'flycheck-irony-setup)
  (if work-computer (setq flycheck-protoc-import-path '("/home/clint/av")))
  (add-hook 'c++-mode-hook
            (lambda()
              (setq flycheck-gcc-language-standard "c++14")
              (setq flycheck-clang-language-standard "c++14")))
  (add-hook 'c-mode-hook
            (lambda()
              (setq flycheck-gcc-language-standard "c11")
              (setq flycheck-clang-language-standard "c11")))
  (use-package flycheck-rust))

Code formatting

Clang format a buffer!

;; C/C++ formatting
(use-package clang-format
  :config
  (evil-leader/set-key-for-mode 'c++-mode "f" 'clang-format-buffer)
  (evil-leader/set-key-for-mode 'c++-mode "F" 'clang-format-region)
  (evil-leader/set-key-for-mode 'c-mode "f" 'clang-format-buffer)
  (evil-leader/set-key-for-mode 'c-mode "F" 'clang-format-region)
  (evil-leader/set-key-for-mode 'protobuf-mode "f" 'clang-format-buffer)
  (evil-leader/set-key-for-mode 'protobuf-mode "F" 'clang-format-region)
  (evil-leader/set-key-for-mode 'glsl-mode "f" 'clang-format-buffer)
  (evil-leader/set-key-for-mode 'glsl-mode "F" 'clang-format-region))

YAPF is a great formatter for Python that actually allows configuration.

;; yapf: python formatting
(use-package yapfify
  :config
  (evil-leader/set-key-for-mode 'python-mode "f" 'yapfify-buffer))

Code completion

I'm primarily a C++ coder on large codebases, so I've mostly given up always getting libclang compile_commands.json working, so I accept the minimal of completion. I use Company (and company-jedi for Python) for what completion I do use.

;; company: code autocomplete
(use-package company
  :defer
  :init (global-company-mode)
  :bind ("TAB" . company-indent-or-complete-common)
  :config
  (setq company-idle-delay 0.1)
  (setq company-minimum-prefix-length 2))

(use-package company-c-headers
  :config
  (add-to-list 'company-c-headers-path-system "/usr/include/c++/8")
  (add-to-list 'company-c-headers-path-system "/usr/include/c++/6")
  (add-to-list 'company-c-headers-path-system "/usr/include/c++/5"))
(add-to-list 'company-backends 'company-c-headers)

(use-package company-jedi
  :config
  (add-to-list 'company-backends 'company-jedi))

;;(use-package company-lua)
;;(use-package company-racer)  ;; rust
;;(use-package company-web)
;;(use-package web-completion-data)

Directory/file tree

Like those nifty directory hierarchy side panels? Neotree! I bind mine to \-s:

;; neotree code directory tree viewer
(use-package neotree
  :config
  (setq neo-smart-open t)
  (evil-leader/set-key "s" 'neotree-toggle)
  (evil-define-key 'normal neotree-mode-map (kbd "TAB") 'neotree-enter)
  (evil-define-key 'normal neotree-mode-map (kbd "SPC") 'neotree-enter)
  (evil-define-key 'normal neotree-mode-map (kbd "RET") 'neotree-enter)
  (evil-define-key 'normal neotree-mode-map (kbd "q") 'neotree-hide))

Major mode packages

(use-package dockerfile-mode :mode "Dockerfile.*\\'")

(use-package groovy-mode)

(use-package markdown-mode
  :mode "\\.md\\'"
  :init
  (setq markdown-command "markdown2")  ;; python3-markdown2
  (add-hook 'markdown-mode-hook (lambda () (set-fill-column 80))))

(use-package nginx-mode
  :mode "/.*/sites-\\(?:available\\|enabled\\)/")

(use-package yaml-mode
  :mode ("\\.yaml\\'"
         "\\.yml\\'"
         "\\.cfg\\'"))

(use-package protobuf-mode
  ;; use my own latest version of protobuf-mode.el from the Protobuf sources.
  :load-path "/home/clint/dotfiles/third_party/"
  :mode "\\.proto\\'")

;; Bazel files are basically Python
(add-to-list 'auto-mode-alist '("\\BUILD\\'" . python-mode))
(add-to-list 'auto-mode-alist '("\\.bzl\\'" . python-mode))

AUCTeX is a powerful package for creating LaTeX documents.

;; AUCTeX (LaTeX)
(use-package tex
  :init
  (setq TeX-auto-save t)
  (setq TeX-parse-self t)
  (setq-default TeX-master nil)
  (setq reftex-plug-into-AUCTeX t)
  (setq TeX-PDF-mode t)
  :config
  (add-hook 'LaTeX-mode-hook 'visual-line-mode)
  (add-hook 'LaTeX-mode-hook 'flyspell-mode)
  (add-hook 'LaTeX-mode-hook 'LaTeX-math-mode)
  (add-hook 'LaTeX-mode-hook 'turn-on-reftex))

Miscellaneous configuration values

;; misc configuration

(setq diff-switches "-u")  ;; unified diffs
(global-linum-mode t)  ;; always show line numbrs
(setq linum-format "%d ")
(setq column-number-mode t)
(setq browse-url-generic-program "firefox")
(setq make-backup-files nil)
(setq gdb-many-windows t)
(show-paren-mode t)  ;; highlight matching braces
(setq show-paren-delay 0)
(setq-default fill-column 100)
;; set default python interpreter
(setq python-shell-interpreter "python3")


;; indentation

(setq-default indent-tabs-mode nil)
(setq tab-width 4)
(setq c-basic-offset tab-width)
(setq c-default-style "linux")
(setq cperl-indent-level tab-width)
(setq lua-indent-level 2)
(setq yaml-indent-offset 2)

Theme, colors, and font

I use Adobe's Source Code Pro font.

I customize my font size based on monitor resolution (my personal computers have way better names than "clint-laptop"):

;; determine whether or not on work computer.
(defconst work-computer (equal (system-name) "clint-laptop"))

(defconst clint-font
  (if work-computer
      "SourceCodePro-10"
    "SourceCodePro-12"))

(if (display-graphic-p)
    (set-frame-font clint-font))

I like Zenburn colors for editors (though I used Solarized for my terminal).

My configuration sets up the theme in different ways depending on if launched in graphical vs. no-window mode so theme colors are loaded correctly per-frame, especially since I start the Emacs daemon in a tty.

(use-package zenburn-theme
  :config
  (if (daemonp)
      (add-hook 'after-make-frame-functions
                (lambda (frame)
                  (select-frame frame)
                  (load-theme 'zenburn t)
                  (when (display-graphic-p frame)
                    (set-frame-font clint-font))))))

Finally, who doesn't like a λ symbol displayed in their sources?

(global-prettify-symbols-mode t)



Note: This blog post is part of #100DaysToOffload.