Skip to content

trevorbernard/emacs.d

Repository files navigation

Emacs Configuration

This is my emacs, there are many like it, but this one is mine…

Introduction

I run Emacs 30.1 exclusively in the terminal (emacs -nw) on both Linux and Mac. Startup speed matters. This configuration uses use-package with global deferred loading and native compilation.

Remap Caps Lock to Control. Your pinkies will thank you.

Installation

git clone git@github.com:trevorbernard/emacs.d.git ~/.emacs.d
cd ~/.emacs.d
make setup

make setup installs all ELPA packages, downloads tree-sitter grammars, and compiles the configuration. Requires Emacs 29+.

Start Emacs with emacs -nw. After editing configuration.org, run make to tangle and recompile.

Bootstrap

Everything here runs before the main configuration loads. The GC threshold is cranked up during init and restored at the end.

Early Init

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

(setq process-adaptive-read-buffering nil
      read-process-output-max (* 10 1024 1024))

(add-hook 'emacs-startup-hook
          (lambda ()
            (setq gc-cons-threshold 16777216
                  gc-cons-percentage 0.1)
            (message "Emacs ready in %s with %d garbage collections."
                     (format "%.2f seconds" (float-time (time-subtract after-init-time before-init-time)))
                     gcs-done)))

(setq gc-cons-threshold most-positive-fixnum
      gc-cons-percentage 0.9)

(set-face-attribute 'mode-line nil :background 'unspecified)
(set-face-attribute 'header-line nil :background 'unspecified)

(menu-bar-mode -1)

(defun display-startup-echo-area-message ())

(setq inhibit-startup-message t)

(when (native-comp-available-p)
  (startup-redirect-eln-cache (expand-file-name "eln-cache/" user-emacs-directory))
  (setq native-comp-speed 2
        native-comp-jit-compilation t
        native-comp-async-jobs-number (min 4 (max 1 (/ (num-processors) 2)))
        native-comp-async-report-warnings-errors nil))

(setenv "LSP_USE_PLISTS" "true")

;; Bootstrap package system and use-package for better startup performance
(require 'package)

(setq package-archives '(("melpa" . "https://melpa.org/packages/")
                         ("melpa-stable" . "https://stable.melpa.org/packages/")
                         ("gnu" . "https://elpa.gnu.org/packages/")))

;; pin projectile to melpa-stable
(setq package-pinned-packages
      '((projectile . "melpa-stable")))

(setq package-enable-at-startup nil)

(package-initialize)

(setq use-package-always-defer t
      use-package-verbose nil  ; Set to t for debugging, nil for performance
      use-package-minimum-reported-time 0.1)
(require 'use-package)

(provide 'early-init)

Preamble

;;; -*- lexical-binding: t -*-
(setq load-prefer-newer t)

(setq user-full-name "Trevor Bernard"
      user-mail-address "trevor.bernard@pm.me")

Appearance

Theme

(use-package timu-spacegrey-theme
  :vc (:url "https://github.com/trevorbernard/timu-spacegrey-theme" :rev :newest)
  :demand t
  :init
  (setq timu-spacegrey-transparent-background t)
  (add-to-list 'custom-theme-load-path
               (expand-file-name "elpa/timu-spacegrey-theme" user-emacs-directory))
  :hook
  (after-init . (lambda () (load-theme 'timu-spacegrey t))))

Mode Line

(use-package doom-modeline
  :ensure t
  :hook (after-init . doom-modeline-mode))

Visual Indicators

(use-package rainbow-delimiters
  :ensure t
  :hook ((prog-mode . rainbow-delimiters-mode)))

Terminal

macOS

(when (eq system-type 'darwin)
 (setq delete-by-moving-to-trash t)
 (setq trash-directory "~/.Trash/emacs")
 (setq dired-use-ls-dired nil))

Mouse & Display

(unless (display-graphic-p)
  (xterm-mouse-mode t)
  (global-set-key (kbd "<wheel-up>") 'scroll-down-line)
  (global-set-key (kbd "<wheel-down>") 'scroll-up-line)
  (set-face-inverse-video 'vertical-border nil)
  (set-face-background 'vertical-border (face-background 'default))
  (set-display-table-slot standard-display-table 'vertical-border (make-glyph-code ?│))
  (unless (fboundp 'x-hide-tip)
    (defun x-hide-tip () nil))
  (send-string-to-terminal "\e[1 q")
  (add-hook 'kill-emacs-hook (lambda () (send-string-to-terminal "\e[5 q"))))

Clipboard

Wayland needs explicit clipboard plumbing for copy/paste between Emacs and other applications. For terminal sessions over SSH, OSC-52 escape sequences let yanked text reach the host clipboard.

(when (getenv "WAYLAND_DISPLAY")
  (setq tb/wl-copy-process nil)
  (defun tb/wl-copy (text)
    (setq tb/wl-copy-process (make-process :name "wl-copy"
                                           :buffer nil
                                           :command '("wl-copy" "-f" "-n")
                                           :connection-type 'pipe
                                           :noquery t))
    (process-send-string tb/wl-copy-process text)
    (process-send-eof tb/wl-copy-process))
  (defun tb/wl-paste ()
    (unless (and tb/wl-copy-process (process-live-p tb/wl-copy-process))
      (shell-command-to-string "wl-paste -n | tr -d \r")))
  (setq interprogram-cut-function 'tb/wl-copy)
  (setq interprogram-paste-function 'tb/wl-paste))
(use-package clipetty
  :ensure t
  :bind ("M-w" . clipetty-kill-ring-save))

Editing

Behavior

(setq
 use-short-answers t
 scroll-preserve-screen-position t
 ring-bell-function 'ignore)

(global-auto-revert-mode t)
(column-number-mode 1)
(delete-selection-mode 1)
(show-paren-mode 1)

File Handling

(setq
 make-backup-files nil
 auto-save-default nil
 create-lockfiles nil)

Indentation

(setq-default indent-tabs-mode nil)
(setq-default c-basic-offset 4)
(setq-default tab-width 8)
(setq-default fill-column 80)
(setq-default truncate-lines nil)

Enabled Commands

(put 'downcase-region 'disabled nil)
(put 'narrow-to-region 'disabled nil)
(put 'upcase-region 'disabled nil)

Navigation & Completion

Ivy & Counsel

(use-package ivy
  :ensure t
  :commands ivy-mode
  :hook (after-init . ivy-mode))

(use-package counsel
  :ensure t
  :commands counsel-mode
  :hook (ivy-mode . counsel-mode))

Projectile

(use-package projectile
  :ensure t
  :diminish projectile-mode
  :custom
  (projectile-project-search-path '("~/p/" "~/code/" "~/.emacs.d/"))
  (projectile-completion-system 'ivy)
  (projectile-enable-caching t)
  (projectile-indexing-method 'alien)
  (projectile-sort-order 'recently-active)
  :bind-keymap ("C-c p" . projectile-command-map)
  :bind (:map projectile-command-map
              ("C" . projectile-invalidate-cache))
  :commands (projectile-find-file projectile-switch-project projectile-mode)
  :config
  (projectile-mode))

Company

(use-package company
  :ensure t
  :bind
  (:map company-active-map
        ("C-n". company-select-next)
        ("C-p". company-select-previous)
        ("M-<". company-select-first)
        ("M->". company-select-last))
  :hook (prog-mode . company-mode))

Key Bindings

M-x via C-x C-m per Steve Yegge’s Effective Emacs. Keeps your fingers on the home row when Caps Lock is mapped to Control.

(keymap-global-set "C-x C-m" 'execute-extended-command)
(keymap-global-set "C-c C-m" 'execute-extended-command)
(keymap-global-set "C-x g" 'magit-status)
(keymap-global-set "C-c g" 'magit-file-dispatch)

Development Tools

prog-mode Defaults

(use-package prog-mode
  :custom
  (display-line-numbers-type 'relative)
  :hook (prog-mode . (lambda ()
                       (setq-local show-trailing-whitespace t)
                       (display-line-numbers-mode))))

Magit

(use-package magit
  :ensure t
  :commands (magit-status magit-file-dispatch))

Paredit

On Mac, ^-left and ^-right conflict with Mission Control. Disable them in System Preferences > Keyboard > Shortcuts > Mission Control.

(use-package paredit
  :ensure t
  :bind
  (:map paredit-mode-map
        ("C-<right>" . paredit-forward-slurp-sexp)
        ("C-<left>" . paredit-forward-barf-sexp)
        ("C-<backspace>" . paredit-backward-kill-word)
        ("RET" . nil))
  :hook ((cider-repl-mode
          clojure-mode
          emacs-lisp-mode
          eval-expression-minibuffer-setup
          ielm-mode
          inf-clojure-mode-hook
          lisp-interaction-mode
          lisp-mode
          scheme-mode) . paredit-mode))

Evil

(use-package evil
  :ensure t
  :commands evil-mode)

Flycheck

(defun tb/python-ruff-setup ()
  "Configure ruff as the Python checker unless in org-src-mode."
  (unless (bound-and-true-p org-src-mode)
    (when (buffer-file-name)
      (flycheck-mode)
      (setq-local flycheck-checkers '(python-ruff)))))

(use-package flycheck
  :ensure t
  :config
  (flycheck-define-checker python-ruff
    "A Python syntax and style checker using the ruff utility.
  To override the path to the ruff executable, set
  `flycheck-python-ruff-executable'.
  See URL `http://pypi.python.org/pypi/ruff'."
    :command ("ruff"
              "check"
              "--output-format=text"
              (eval (when buffer-file-name
                      (concat "--stdin-filename=" buffer-file-name)))
              "-")
    :standard-input t
    :error-filter (lambda (errors)
                    (let ((errors (flycheck-sanitize-errors errors)))
                      (seq-map #'flycheck-flake8-fix-error-level errors)))
    :error-patterns
    ((warning line-start
              (file-name) ":" line ":" (optional column ":") " "
              (id (one-or-more (any alpha)) (one-or-more digit)) " "
              (message (one-or-more not-newline))
              line-end))
    :modes (python-mode python-ts-mode))

  :hook ((python-mode . tb/python-ruff-setup)
         (rust-mode . flycheck-mode))

  :bind (:map flycheck-mode-map
              ("M-n" . flycheck-next-error)
              ("M-p" . flycheck-previous-error)))

Flyspell

(use-package flyspell
  :commands (flyspell-mode flyspell-prog-mode)
  :bind (:map flyspell-mouse-map
              ([down-mouse-3] . flyspell-correct-word)
              ([mouse-3] . undefined))
  :hook (((org-mode markdown-mode) . flyspell-mode))
  :config
  (setq flyspell-issue-welcome-flag nil
        flyspell-issue-message-flag nil
        flyspell-mark-duplications-flag nil
        ispell-program-name "aspell"
        ispell-list-command "list"))

Tree-sitter

Emacs 29+ has built-in tree-sitter support. This configures grammars for all languages and remaps traditional major modes to their tree-sitter equivalents.

(use-package treesit
  :mode (("\\.tsx\\'" . tsx-ts-mode)
         ("\\.js\\'"  . typescript-ts-mode)
         ("\\.mjs\\'" . typescript-ts-mode)
         ("\\.mts\\'" . typescript-ts-mode)
         ("\\.cjs\\'" . typescript-ts-mode)
         ("\\.ts\\'"  . typescript-ts-mode)
         ("\\.jsx\\'" . tsx-ts-mode)
         ("\\.json\\'" .  json-ts-mode)
         ("\\.yaml\\'" .  yaml-ts-mode)
         ("\\.Dockerfile\\'" . dockerfile-ts-mode))
  :preface
  (defvar os/treesit-grammars-installed nil
    "Cache variable to track if tree-sitter grammars have been checked/installed.")

  (defvar os/treesit-grammar-cache-file
    (expand-file-name "treesit-grammars-installed" user-emacs-directory)
    "File to persist tree-sitter grammar installation status.")

  (defun os/setup-install-grammars ()
    "Install Tree-sitter grammars if they are absent.
Uses caching to avoid checking on every startup - only runs once per session
or when explicitly called interactively."
    (interactive)
    (when (and (fboundp 'treesit-available-p)
               (treesit-available-p)
               (or (called-interactively-p 'any)
                   (not os/treesit-grammars-installed)
                   (not (file-exists-p os/treesit-grammar-cache-file))))
      (unless (boundp 'treesit-language-source-alist)
        (setq treesit-language-source-alist nil))

      (let ((grammars-to-install '())
            (grammar-sources '((css . ("https://github.com/tree-sitter/tree-sitter-css" "v0.20.0"))
                               (scss . ("https://github.com/serenadeai/tree-sitter-scss"))
                               (bash "https://github.com/tree-sitter/tree-sitter-bash")
                               (html . ("https://github.com/tree-sitter/tree-sitter-html" "v0.20.1"))
                               (javascript . ("https://github.com/tree-sitter/tree-sitter-javascript" "v0.21.2" "src"))
                               (java . ("https://github.com/tree-sitter/tree-sitter-java"))
                               (json . ("https://github.com/tree-sitter/tree-sitter-json" "v0.20.2"))
                               (python . ("https://github.com/tree-sitter/tree-sitter-python" "v0.20.4"))
                               (go "https://github.com/tree-sitter/tree-sitter-go" "v0.20.0")
                               (markdown "https://github.com/ikatyang/tree-sitter-markdown")
                               (make "https://github.com/alemuller/tree-sitter-make")
                               (elisp "https://github.com/Wilfred/tree-sitter-elisp")
                               (cmake "https://github.com/uyha/tree-sitter-cmake")
                               (c . ("https://github.com/tree-sitter/tree-sitter-c" "v0.20.7"))
                               (cpp "https://github.com/tree-sitter/tree-sitter-cpp")
                               (toml "https://github.com/tree-sitter/tree-sitter-toml")
                               (tsx . ("https://github.com/tree-sitter/tree-sitter-typescript" "v0.20.3" "tsx/src"))
                               (typescript . ("https://github.com/tree-sitter/tree-sitter-typescript" "v0.20.3" "typescript/src"))
                               (yaml . ("https://github.com/ikatyang/tree-sitter-yaml" "v0.5.0"))
                               (rust . ("https://github.com/tree-sitter/tree-sitter-rust" "v0.24.1" "src"))
                               (just "https://github.com/IndianBoy42/tree-sitter-just")
                               (ruby "https://github.com/tree-sitter/tree-sitter-ruby"))))

        (dolist (grammar grammar-sources)
          (add-to-list 'treesit-language-source-alist grammar)
          (unless (treesit-language-available-p (car grammar))
            (push grammar grammars-to-install)))

        (when grammars-to-install
          (message "Installing %d missing tree-sitter grammars..." (length grammars-to-install))
          (dolist (grammar grammars-to-install)
            (condition-case err
                (treesit-install-language-grammar (car grammar))
              (error (message "Failed to install grammar %s: %s" (car grammar) err)))))

        (setq os/treesit-grammars-installed t)
        (with-temp-file os/treesit-grammar-cache-file
          (insert (format "Last checked: %s\n" (current-time-string))))
        (when (called-interactively-p 'any)
          (message "Tree-sitter grammar check completed.")))))

  (dolist (mapping
           '((bash-mode . bash-ts-mode)
             (c++-mode . c++-ts-mode)
             (c-mode . c-ts-mode)
             (c-or-c++-mode . c-or-c++-ts-mode)
             (css-mode . css-ts-mode)
             (java-mode . java-ts-mode)
             (js-json-mode . json-ts-mode)
             (js-mode . typescript-ts-mode)
             (js2-mode . typescript-ts-mode)
             (json-mode . json-ts-mode)
             (python-mode . python-ts-mode)
             (scss-mode . scss-ts-mode)
             (sh-base-mode . bash-ts-mode)
             (sh-mode . bash-ts-mode)
             (ruby-mode . ruby-ts-mode)
             (typescript-mode . typescript-ts-mode)))
    (add-to-list 'major-mode-remap-alist mapping))
  :config
  (add-hook 'after-init-hook
            (lambda () (run-with-idle-timer 2.0 nil #'os/setup-install-grammars))))

LSP

Uses emacs-lsp-booster when available to speed up JSON parsing.

(use-package lsp-ivy
  :ensure t
  :after (lsp-mode ivy)
  :commands lsp-ivy-workspace-symbol)

(use-package lsp-ui
  :ensure t
  :after lsp-mode
  :commands lsp-ui-mode
  :hook (lsp-mode . lsp-ui-mode)
  :config
  (setq lsp-ui-doc-enable nil))

(use-package lsp-mode
  :ensure t
  :commands (lsp lsp-deferred)
  :hook ((tsx-ts-mode typescript-ts-mode js-ts-mode python-ts-mode java-ts-mode) . lsp-deferred)
  :init
  (setq lsp-log-io nil
        lsp-use-plists t)
  :config
  (defun lsp-booster--advice-json-parse (old-fn &rest args)
    (or
     (when (equal (following-char) ?#)
       (let ((bytecode (read (current-buffer))))
         (when (byte-code-function-p bytecode)
           (funcall bytecode))))
     (apply old-fn args)))

  (defun lsp-booster--advice-final-command (old-fn cmd &optional test?)
    (let ((orig-result (funcall old-fn cmd test?)))
      (if (and (not test?)
               (not (file-remote-p default-directory))
               lsp-use-plists
               (not (functionp 'json-rpc-connection))
               (executable-find "emacs-lsp-booster"))
          (progn
            (when-let ((resolved (executable-find (car orig-result))))
              (setcar orig-result resolved))
            (message "Using emacs-lsp-booster for %s!" orig-result)
            (cons "emacs-lsp-booster" orig-result))
        orig-result)))

  (advice-add (if (fboundp 'json-parse-buffer) 'json-parse-buffer 'json-read)
              :around #'lsp-booster--advice-json-parse)
  (advice-add 'lsp-resolve-final-command :around #'lsp-booster--advice-final-command))

Structural Editing

(use-package combobulate
   :vc (:url "https://github.com/mickeynp/combobulate" :rev :newest)
   :custom
   (combobulate-key-prefix "C-c o")
   :hook ((python-ts-mode
           tsx-ts-mode
           typescript-ts-mode
           js-ts-mode
           css-ts-mode
           yaml-ts-mode
           json-ts-mode
           go-ts-mode
           html-ts-mode
           toml-ts-mode) . combobulate-mode))

(use-package indent-bars
  :vc (:url "https://github.com/jdtsmith/indent-bars" :rev :newest)
  :hook ((python-ts-mode
          tsx-ts-mode
          typescript-ts-mode
          js-ts-mode
          css-ts-mode
          yaml-ts-mode
          json-ts-mode
          go-ts-mode
          html-ts-mode
          toml-ts-mode
          rust-ts-mode
          java-ts-mode
          c-ts-mode
          c++-ts-mode
          bash-ts-mode) . indent-bars-mode))

Snippets & Search

(use-package yasnippet
  :ensure t
  :diminish yas-minor-mode
  :commands (yas-minor-mode yas-global-mode)
  :hook ((prog-mode . yas-minor-mode)
         (org-mode . yas-minor-mode)))

(use-package ag
  :ensure t
  :commands (ag ag-project ag-regexp))

(use-package string-inflection
  :ensure t
  :commands (string-inflection-all-cycle))

IELM

(use-package ielm
  :hook ((ielm-mode . paredit-mode))
  :bind
  (:map ielm-map
        ("C-m" . ielm-return)
        ("<return>" . ielm-return)))

Languages

Clojure

Cider is pinned to melpa-stable to avoid bleeding-edge breakage.

(use-package clojure-mode
  :ensure t
  :hook (clojure-mode . subword-mode)
  :custom
  (clojure-align-forms-automatically t)
  :config
  (eldoc-add-command 'paredit-backward-delete 'paredit-close-round))

(use-package cider
  :ensure t
  :commands cider-jack-in
  :bind ("C-c C-j" . cider-jack-in)
  :custom
  (nrepl-log-messages t)
  (cider-repl-use-clojure-font-lock t)
  (cider-repl-display-help-banner nil))

Rust

(use-package rust-mode
  :ensure t
  :init
  (setq rust-mode-treesitter-derive t))

(use-package rustic
  :ensure t
  :after (rust-mode)
  :bind (:map rustic-mode-map
              ("M-j" . lsp-ui-imenu)
              ("M-?" . lsp-find-references)
              ("C-c C-c l" . flycheck-list-errors)
              ("C-c C-c a" . lsp-execute-code-action)
              ("C-c C-c r" . lsp-rename)
              ("C-c C-c q" . lsp-workspace-restart)
              ("C-c C-c Q" . lsp-workspace-shutdown)
              ("C-c C-c s" . lsp-rust-analyzer-status))
  :custom
  (rustic-compile-command "cargo b --release")
  (rustic-default-clippy-arguments "--all-targets --all-features -- -D warnings")
  (rust-format-on-save t)
  (rustic-ansi-faces ["black" "#bf616a" "#a3be8c" "#ecbe7b" "#2257a0" "#b48ead" "#4db5bd" "white"]))

Org

C-j is rebound from org-return-indent to org-return because I use C-j in place of the enter key everywhere.

(use-package ob-rust
  :ensure t
  :after org)

(use-package org
  :bind
  (:map
   org-mode-map
   ("C-j" . org-return)
   ("C-c ]" . org-ref-insert-link)
   ("C-c l" . org-store-link)
   ("C-c a" . org-agenda)
   ("C-c c" . org-capture))
  :hook (org-mode . auto-fill-mode)
  :config
  (set-face-attribute 'org-block nil :background 'unspecified)
  (set-face-attribute 'org-block-begin-line nil :background 'unspecified)
  (set-face-attribute 'org-block-end-line nil :background 'unspecified)
  (org-babel-do-load-languages
   'org-babel-load-languages '((rust . t)
                               (shell . t))))

Exporting to PDF requires basictex:

brew reinstall --cask basictex
sudo tlmgr update --self
sudo tlmgr install wrapfig
sudo tlmgr install capt-of

Nix

(use-package lsp-nix
  :ensure lsp-mode
  :after (lsp-mode)
  :custom
  (lsp-nix-nil-formatter ["nixfmt"]))

(use-package nix-mode
  :ensure t
  :hook (nix-mode . lsp-deferred))

(use-package nixfmt
  :ensure t
  :bind
  (:map
   nix-mode-map
   ("C-c C-f" . nixpkgs-fmt-buffer)))

Ruby

(use-package ruby-ts-mode
  :mode "\\.rb\\'"
  :mode "Rakefile\\'"
  :mode "Gemfile\\'"
  :custom
  (ruby-indent-level 2)
  (ruby-indent-tabs-mode nil))

Java

(use-package lsp-java
  :ensure t
  :after lsp-mode
  :hook (java-ts-mode . lsp))

(use-package dap-java :after (lsp-java))

Terraform

(use-package terraform-mode
  :ensure t
  :hook (terraform-mode . lsp-deferred))

File Modes

(use-package js
  :custom
  (js-indent-level 2))

(use-package css-mode
  :custom
  (css-indent-level 2))

(use-package markdown-mode
  :ensure t
  :mode (("\\.md\\'" . gfm-mode)
         ("\\.markdown\\'" . gfm-mode))
  :config
  (set-face-attribute 'markdown-code-face nil :background 'unspecified)
  (set-face-attribute 'markdown-pre-face nil :background 'unspecified)
  (set-face-attribute 'markdown-inline-code-face nil :background 'unspecified))

(use-package csv-mode
  :ensure t
  :mode "\\.csv\\'")

(use-package dockerfile-mode
  :ensure t
  :commands dockerfile-mode)

(use-package yaml-mode
  :ensure t
  :commands yaml-mode)

(use-package bnf-mode
  :ensure t
  :mode "\\.bnf\\'")

(use-package dotenv-mode
  :ensure t
  :mode "\\.env\\'")

(use-package just-ts-mode
  :ensure t
  :hook (just-ts-mode . (lambda ()
                          (setq-local just-ts-indent-offset 2
                                      tab-width 2))))

(use-package pest-mode
  :ensure t
  :mode "\\.pest\\'"
  :hook (pest-mode . flycheck-mode))

(use-package flycheck-pest
  :ensure t
  :after pest-mode)

Epilogue

The GC threshold is restored by emacs-startup-hook in early-init.

About

This is my emacs, there are many like it, but this one is mine...

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors