Skip to content

Latest commit

 

History

History
843 lines (675 loc) · 28.7 KB

init.org

File metadata and controls

843 lines (675 loc) · 28.7 KB

Bootstrapping and package management

Tangling

There are two main approaches to writing your emacs configuration in org. I’m choosing to run org-babel-load-file manually after editing this file. It’s a bit tedious, but preferable to imposing startup delays.

Customize

I’d ideally like to never do anything that invokes Customize options, but this setup isn’t there yet. I want to be able to clone this repository to ~/.emacs.d and run org-babel-load-file to generate an init.el in place that is used to boot emacs from then on. At present, doing this generates custom options that are written to ~/.emacs. The presence of that file then blocks emacs from using ~/.emacs.d/init.el during startup.

Declaring a file, just for Customize options, works around this.

(setq custom-file (expand-file-name "custom.el" user-emacs-directory))
(load custom-file)

Package management

I use straight and use-package to manage packages.

(setq load-prefer-newer t)
(setq straight-repository-branch "master")
(setq straight-check-for-modifications '(find-when-checking))
(setq straight-use-package-by-default t)
(setq straight-cache-autoloads t)
(setq use-package-inject-hooks t)

(let ((bootstrap-file
  (expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
  (bootstrap-version 5))
(unless (file-exists-p bootstrap-file)
  (with-current-buffer
      (url-retrieve-synchronously
       "https://raw.githubusercontent.com/raxod502/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)

Run M-x straight-pull-all to update all packages, and then run M-x straight-freeze-versions to save the currently checked out revisions of all packages. The ~~/.emacs.d/straight/versions/default.el~ file, together with this file, define the package configuration.

Persistent editing

Run emacs as a server.

(server-start)

I put an executable script in ~/bin to invoke emacs from a shell and have the single instance receive calls to it. I use the d12frosted/emacs-plus and emacsclient packages.

#!/bin/sh

/opt/homebrew/bin/emacsclient -n $@

Code formatting

Reformatter runs command-line formatters to automatically format code in a buffer.

(straight-use-package 'reformatter)

Core UI

Chrome

I don’t want distractions like the startup screen, the toolbar or scrollbars. I want the modeline to show line and column numbers. Don’t waste my time asking me to type yes and no when y or n will suffice.

(setq inhibit-startup-screen t)
(setq initial-scratch-message nil)
(tool-bar-mode -1)
(scroll-bar-mode -1)
(setq line-number-mode 1)
(setq column-number-mode 1)
(defalias 'yes-or-no-p 'y-or-n-p)

I don’t want a spurious Cmd-q to kill emacs on macOS.

(setq confirm-kill-emacs 'yes-or-no-p)

I never want to hear a single peep out of my editor.

(setq ring-bell-function #'ignore)

Theme

I’ve tried several themes, mostly within the base16 family.

(use-package base16-theme
  :init
  (load-theme 'base16-irblack t))
(defun get-base16-color (id)
  (plist-get base16-irblack-theme-colors id))

Fonts and faces

I use set-face-attribute for global faces instead of custom-set-faces because I want to avoid using the Customize interface, which would mutate my init file. It lets me twiddle any individual part of any face (see the full list of attributes) without going through Customize. For package-specific faces, use-package offers the :custom-face keyword, which goes through Customize while avoiding its major downside.

(cond ((eq system-type 'gnu/linux)
       (set-face-attribute 'default nil :family "Input"
                                        :height 100))
      ;; ((eq system-type 'darwin)
      ;;  (set-face-attribute 'default nil :family "Monaco"
      ;;                                   :height 160)))
      ((eq system-type 'darwin)
       (set-face-attribute 'default nil :family "Source Code Pro"
                                        :height 180)))
(set-face-attribute 'fixed-pitch nil :family 'unspecified
                                     :inherit 'default)

Environment

exec-path-from-shell ensures that Emacs.app on macOS uses the same paths as my shell environment.

(use-package exec-path-from-shell
  :if (eq system-type 'darwin)
  :custom
  (exec-path-from-shell-check-startup-files nil)
  (exec-path-from-shell-variables '("PATH"
                                    "MANPATH"
                                    "GOPATH"
                                    "GOPRIVATE"
                                    "OPENAI_API_KEY"))
  :config
  (exec-path-from-shell-initialize))

Clipboard

I want emacs to fit in naturally with the rest of my environment and give me access to functionality I don’t otherwise have, if the ergonomics are good. Saving clipboard contents to the kill ring provides access to data that is otherwise easily lost.

(setq save-interprogram-paste-before-kill t)

Files and buffers

I never run more than one copy of emacs, nor do I ever make use of backup files. Don’t keep junk lying around if it’ll never be used.

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

Uniquify buffer names using a style that matches file paths as much as possible.

(setq uniquify-buffer-name-style 'forward)

I prefer ibuffer to the builtin buffer menu functionality.

(global-set-key (kbd "C-x C-b") 'ibuffer)

I like closing all buffers to reset emacs to a clean slate when I switch from one task to another. Using C-x C-b % n <ENTER> D is pretty tedious. M-x close-all-buffers is a touch easier.

(defun close-all-buffers ()
  (interactive)
  (mapc 'kill-buffer (buffer-list)))

Show the full path to the file in the current buffer in the window title.

(setq frame-title-format
  (list '(buffer-file-name "%f" (dired-directory dired-directory "%b"))))

Scrolling

Scroll one line at a time. The default behaviour is jarring.

(setq scroll-conservatively 1)

Line numbers

Show line numbers to make it easier to pair with others.

(global-display-line-numbers-mode t)
(setq display-line-numbers-width 3)

Long lines and whitespace

I prefer spaces to tabs and use a single space after a period. I also want code and text to fit within 80 characters whenever reasonable.

(setq sentence-end-double-space nil)
(setq-default indent-tabs-mode nil)
(setq-default fill-column 80)
(setq default-tab-width 4)
(setq tab-width 4)

Ideally, formatting would be taken care of by tooling that limits length whenever reasonable. highlight-80+-mode highlights lines that exceed 80 characters. This provides a useful signal to think about whether the line should be broken up or not.

The builtin whitespace-mode can do this now. It’d be good to switchover to it and drop this unmaintained package.

(use-package highlight-80+
  :straight (highlight-80+ :type git :host github :repo "jkakar/highlight-80-mode"))
(setq highlight-80+-columns 81)
(set-face-attribute 'highlight-80+ nil :foreground 'unspecified
                                       :background (get-base16-color ':base01))

I don’t want to leave trailing whitespace in files. ws-butler only deletes tailing whitespace from edited lines, which helps keeps diffs clean.

(setq-default show-trailing-whitespace t)
(use-package ws-butler
  :demand t
  :config
  (setq ws-butler-keep-whitespace-before-point nil)
  (ws-butler-global-mode 1))

Line movement

The builtin move-beginning-of-line function jumps to the beginning of the line, but most of the time I want to move to the first non-whitespace character. crux-move-beginning-of-line moves to the first non-whitespace character on the line, or if the point is already there, to the beginning of the line. Invoking it repeatedly toggles between these positions.

(use-package crux
 :bind (("C-a" . crux-move-beginning-of-line)))

Joining lines

Using C-u M-^ to join one line to another is really tedious. Let’s make this easier.

(defun join-next-line ()
  (interactive)
  (join-line t))
(global-set-key (kbd "C-j") 'join-next-line)

Matching parentheses

Highlight all the text between matching parentheses without any delay.

(set-face-attribute 'show-paren-match nil :foreground 'unspecified
                                          :background (get-base16-color ':base01))
(setq show-paren-delay 0)
(setq show-paren-style (quote expression))
(show-paren-mode 1)

Navigation

Ivy, counsel and swiper provide a simple and unified way to quickly navigate buffers, find files, etc.

(use-package swiper
  :config
  (global-set-key (kbd "C-s") 'swiper))
(use-package counsel
  :config
  (global-set-key (kbd "M-x") 'counsel-M-x)
  (global-set-key (kbd "C-x C-f") 'counsel-find-file)
  (global-set-key (kbd "C-c f") 'counsel-fzf)
  (global-set-key (kbd "C-c k") 'counsel-rg)
  (define-key minibuffer-local-map (kbd "C-r") 'counsel-minibuffer-history)
  (setenv "FZF_DEFAULT_COMMAND" "git ls-files --exclude-standard --others --cached")
  (setq counsel-git-cmd "rg --files")
  (setq counsel-async-filter-update-time 100000)
  (setq counsel-rg-base-command "rg -i -M 120 --no-heading --line-number --color never %s ."))
(use-package ivy
  :init (setq ivy-use-virtual-buffers t
              ivy-count-format "(%d/%d) ")
  :bind (("C-c C-r" . ivy-resume)
         :map ivy-minibuffer-map ("RET" . ivy-alt-done))
  :config
  (global-set-key (kbd "C-c C-r") 'ivy-resume)
  (setq ivy-height 15)
  (ivy-mode 1))

I want counsel-M-x to show me the most recently used commands. Installing smex makes this the default behaviour.

(use-package smex)

I mainly use projectile for fuzzy searching an entire project’s files and buffers. It’s quite refreshing to never think about which files are open and which ones aren’t. The concept of a root directory is also important for things like rg searching.

(use-package projectile
  :custom
  (projectile-globally-ignored-file-suffixes '(".pdf"))
  (projectile-globally-unignored-files '(".projectile" ".dir-locals.el"))
  :demand t
  :config
  (setq projectile-enable-caching t)
  (setq projectile-indexing-method 'alien)
  (projectile-mode 1))

I want to be able to jump to any file quickly without having to navigate through directories by hand. counsel-projectile provides a nice way to do this.

(use-package counsel-projectile
 :demand t
 :config
 (counsel-projectile-mode 1))

Git

I frequently want to share a GitHub link to code I’m working with in emacs. Navigating to files and selecting lines in the browser is rather tedious. git-link provides a way to quickly generate GitHub (and other code hosting service) URLs.

(use-package git-link
  :config
  (global-set-key (kbd "C-c g l") 'git-link))

Jump to definition

dump-jump uses brute force very effectively. It provides decent jump to definition behaviour while avoiding the tedium that comes with managing TAGS files and such. I’ve found rg provides the best results.

(use-package dumb-jump
  :bind (("M-g o" . dumb-jump-go-other-window)
         ("M-g j" . dumb-jump-go)
         ("M-g i" . dumb-jump-go-prompt)
         ("M-g x" . dumb-jump-go-prefer-external)
         ("M-g z" . dumb-jump-go-prefer-external-other-window))
  :config (setq dumb-jump-force-searcher 'rg)
          (setq dumb-jump-max-find-time 10)
          (setq dumb-jump-selector 'ivy))

Multi-line editing

wgrep integrates with ivy-occur to provide multi-line editing capabilities.

(use-package wgrep)

Search for text you want to edit, hit C-o C-o (ivy-occur) to open the matches in a buffer. Use C-x C-q (ivy-wgrep-change-to-wgrep-mode) in the buffer to switch into editing mode. Finally, use C-c C-c (wgrep-finish-edit) to apply the changes.

Compilation buffers

Enable ANSI colors in compilation buffers.

(defun colorize-compilation-buffer ()
  (toggle-read-only)
  (ansi-color-apply-on-region compilation-filter-start (point))
  (toggle-read-only))

(add-hook 'compilation-filter-hook 'colorize-compilation-buffer)

LSP

(use-package lsp-mode
  :commands lsp)
(use-package lsp-ui :commands lsp-ui-mode)
(use-package company-lsp :commands company-lsp)
(use-package lsp-ivy :straight (lsp-ivy :type git :host github :repo "emacs-lsp/lsp-ivy"))

(setq gc-cons-threshold 100000000)
(setq lsp-enable-doc t)
(setq lsp-enable-snippet nil)
(setq lsp-idle-delay 0.500)
(setq lsp-prefer-flymake :none)
(setq lsp-ui-doc-enable t)
(setq read-process-output-max (* 1024 1024))
(global-set-key (kbd "C-c h") 'lsp-ui-doc-glance)

Define C-c C-d and C-c C-g to show and hide LSP UI docs, respectively.

(define-key lsp-mode-map (kbd "C-c C-d") 'lsp-ui-doc-show)
(define-key lsp-mode-map (kbd "C-c C-g") 'lsp-ui-doc-hide)

LLM

The GPTel package provides commands for interacting with LLMs to respond to prompts, explain snippets, generate tests, and more.

(use-package gptel)
(global-set-key (kbd "C-c RET") 'gptel-send)

The Copilot package provides completions using GitHub Copilot. Use M-x copilot-install-server and M-x copilot-login to get started.

;; (use-package copilot
;;   :straight (:host github :repo "copilot-emacs/copilot.el" :files ("*.el"))
;;   :ensure t)
;; (add-hook 'prog-mode-hook 'copilot-mode)
;; (define-key copilot-completion-map (kbd "<tab>") 'copilot-accept-completion)
;; (define-key copilot-completion-map (kbd "TAB") 'copilot-accept-completion)

;; (defun jkakar/no-copilot-mode ()
;;   "Helper for `jkakar/no-copilot-modes'."
;;   (copilot-mode -1))

;; (defvar jkakar/no-copilot-modes '(shell-mode
;;                                   inferior-python-mode
;;                                   eshell-mode
;;                                   term-mode
;;                                   vterm-mode
;;                                   comint-mode
;;                                   compilation-mode
;;                                   debugger-mode
;;                                   dired-mode-hook
;;                                   compilation-mode-hook
;;                                   flutter-mode-hook
;;                                   minibuffer-mode-hook)
;;   "Modes in which copilot is inconvenient.")

;; (defun jkakar/copilot-disable-predicate ()
;;   "When copilot should not automatically show completions."
;;   (or jkakar/copilot-manual-mode
;;       (member major-mode jkakar/no-copilot-modes)
;;       (company--active-p)))

;; (add-to-list 'copilot-disable-predicates #'jkakar/copilot-disable-predicate)

https://robert.kra.hn/posts/2023-02-22-copilot-emacs-setup/ has useful information about customized the copilot.el package.

Major modes and filetypes

Dockerfile

(use-package dockerfile-mode
  :config
  (add-hook 'dockerfile-mode-hook 'highlight-80+-mode))

Elixir

(use-package elixir-mode
  :commands elixir-mode
  :config
  (add-hook 'elixir-mode-hook 'highlight-80+-mode))

Erlang

(reformatter-define erlfmt
  :program "/Users/jkakar/bin/erlfmt"
  :args '("-"))
(use-package erlang
  :init
  (add-to-list 'auto-mode-alist '("\\.P\\'" . erlang-mode))
  (add-to-list 'auto-mode-alist '("\\.E\\'" . erlang-mode))
  (add-to-list 'auto-mode-alist '("\\.S\\'" . erlang-mode))
  :config
  (add-hook 'erlang-mode-hook 'highlight-80+-mode)
  (add-hook 'erlang-mode-hook
    (lambda ()
      (setq mode-name "erl"
            erlang-compile-extra-opts '((i . "../include"))
            erlang-root-dir "/usr/local/lib/erlang"))
  (add-hook 'erlang-mode-hook #'lsp))
  ;; TODO Figure out how to turn this off for *.yrl files.
  (add-hook 'erlang-mode-hook 'erlfmt-on-save-mode))
;;(use-package edts
;;  :init
;;  (setq edts-inhibit-package-check t
;;        edts-man-root "~/.emacs.d/edts/doc/18.2.1"))

Flycheck

(use-package flycheck
  :init
  ;; (setq flycheck-ruby-executable (expand-file-name "~/.rbenv/shims/ruby"))
  ;; (setq flycheck-ruby-rubocop-executable (expand-file-name "~/.rbenv/shims/rubocop"))
  (setq flycheck-erlang-include-path '("../include"))
  (setq flycheck-erlang-library-path '())
  (add-hook 'ruby-mode-hook (lambda () (flycheck-disable-checker 'ruby-reek)))
  :config
  (setq-default flycheck-disabled-checkers
                (append flycheck-disabled-checkers
                        '(javascript-jshint)
                        '(json-jsonlist)))
  (global-flycheck-mode))
(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
  :ensure t)
(use-package go-ts-mode
  :config
  (setq gofmt-command "goimports")
  (setq-default tab-width 4)
  (add-hook 'go-ts-mode-hook #'lsp-deferred)
  :custom
  (gofmt-show-errors nil)
  :hook
  (go-ts-mode . (lambda () (add-hook 'before-save-hook 'gofmt-before-save nil t)))
  (go-ts-mode . (lambda ()
                  (setq tab-width 4)
                  (setq go-ts-mode-indent-offset tab-width)
                  (setq indent-tabs-mode t)))
  :init
  (add-hook 'go-ts-mode-hook (lambda () (setq tab-width 4)))
  (add-hook 'go-ts-mode-hook 'highlight-80+-mode)
  (add-hook 'go-ts-mode-hook #'lsp-deferred)
  (add-hook 'go-ts-mode-hook #'lsp-go-install-save-hooks)
  (add-hook 'go-ts-mode-local-vars-hook #'lsp!)
  :defer t)

We install go-mode because it defines the gofmt-before-save hook. We don’t want to add gofmt-before-save to the global before-save-hook, because that would cause go-mode to be loaded in every buffer, whether it was a Go buffer or not. Instead we add to the local before-save-hook. We then have to explicitly request deferred loading. Normally :hook implies :defer t, but only if the target of the hook is a function symbol. If it’s a lambda, then use-package will resort to its default behavior of demanding the package, to ensure that the package is loaded when the lambda runs. In our case, we know the lambda doesn’t need that, so we can safely ask for deferral.

Graphviz

(use-package graphviz-dot-mode
  :init
  (add-hook 'graphviz-dot-mode-hook 'highlight-80+-mode)
  (add-hook 'graphviz-dot-mode-hook (lambda () (setq tab-width 4))))

Javascript and Typescript

(defun biome-format-and-save ()
  "Format the current file with Biome and refresh the buffer."
  (interactive)
  (let ((current-file (buffer-file-name)))
    (when current-file
      (let ((exit-code (call-process "biome" nil nil nil "check" "--apply" current-file)))
        (if (eq exit-code 0)
            (progn
              (message "Formatting successful, reverting buffer.")
              (revert-buffer t t t))
          (message "Error formatting file with Biome."))))))

(defun setup-js-ts-modes ()
  (add-hook 'after-save-hook 'biome-format-and-save nil t))

;; Apply this setup for js, ts, jsx, and tsx files
(add-hook 'js-ts-mode-hook 'setup-js-ts-modes)
(add-hook 'tsx-ts-mode-hook 'setup-js-ts-modes)
(add-hook 'typescript-ts-mode-hook 'setup-js-ts-modes)
(add-hook 'js-mode-hook 'setup-js-ts-modes)  ;; Assuming js-mode for JavaScript
(add-hook 'js-ts-mode-hook (lambda () (setq js-indent-level 2)))
(add-hook 'js-ts-mode-hook #'lsp)
(add-hook 'tsx-ts-mode-hook (lambda () (setq js-indent-level 2)))
(add-hook 'tsx-ts-mode-hook #'lsp)
(add-hook 'typescript-ts-mode-hook (lambda () (setq js-indent-level 2)))
(add-hook 'typescript-ts-mode-hook #'lsp)
(add-hook 'tsx-ts-mode-hook 'auto-revert-mode)
(add-hook 'typescript-ts-mode-hook 'auto-revert-mode)
(add-hook 'js-ts-mode-hook 'auto-revert-mode)
(use-package treesit-auto
  :custom
  (treesit-auto-install 'prompt)
  :config
  (treesit-auto-add-to-auto-mode-alist 'all)
  (global-treesit-auto-mode))

;; this fixes a problem where v0.20.4 of this grammar blows up with emacs
(defvar jkakar/tsx-treesit-auto-recipe
  (make-treesit-auto-recipe
   :lang 'tsx
   :ts-mode 'tsx-ts-mode
   :remap '(typescript-tsx-mode)
   :requires 'typescript
   :url "https://github.com/tree-sitter/tree-sitter-typescript"
   :revision "v0.20.3"
   :source-dir "tsx/src"
   :ext "\\.tsx\\'")
  "Recipe for libtree-sitter-tsx.dylib")
(add-to-list 'treesit-auto-recipe-list jkakar/tsx-treesit-auto-recipe)

(defvar jkakar/typescript-treesit-auto-recipe
  (make-treesit-auto-recipe
   :lang 'typescript
   :ts-mode 'typescript-ts-mode
   :remap 'typescript-mode
   :requires 'tsx
   :url "https://github.com/tree-sitter/tree-sitter-typescript"
   :revision "v0.20.3"
   :source-dir "typescript/src"
   :ext "\\.ts\\'")
  "Recipe for libtree-sitter-typescript.dylib")
(add-to-list 'treesit-auto-recipe-list jkakar/typescript-treesit-auto-recipe)

(use-package typescript-ts-mode
  :config
  (add-hook 'typescript-ts-mode-hook #'lsp)
  :mode
  (("\\.ts\\'" . typescript-ts-mode)
   ("\\.mts\\'" . typescript-ts-mode)
    ("\\.tsx\\'" . tsx-ts-mode)))

C-c C-l is the keyboard shortcut for markdown-insert-link which is needed to edit URLs because markdown-hide-urls is enabled.

(use-package markdown-mode
  :custom
  (markdown-hide-urls t)
  :init
  (add-hook 'markdown-mode-hook 'highlight-80+-mode))

Protocol buffers

(use-package protobuf-mode
  :init
  (defconst my-protobuf-style '((c-basic-offset . 2) (indent-tabs-mode . nil)))
  (add-hook 'protobuf-mode-hook (lambda () (c-add-style "my-style" my-protobuf-style t)))
  (add-hook 'protobuf-mode-hook 'highlight-80+-mode))

Python

(use-package python-mode
  :init
  (add-hook 'python-mode-hook 'highlight-80+-mode))

Ruby

(use-package ruby-mode
  :init
  (add-to-list 'auto-mode-alist '("\\.\\(?:cap\\|gemspec\\|irbrc\\|gemrc\\|rake\\|rb\\|rbi\\|ru\\|thor\\)\\'" . ruby-mode))
  (add-hook 'ruby-mode-hook 'highlight-80+-mode)
  :config
  (setq ruby-insert-encoding-magic-comment nil))
;; (add-hook 'ruby-mode-hook #'lsp)
(use-package rust-mode
  :custom
  (rust-format-on-save t)
  :defer t)
(use-package flycheck-rust
  :hook (rust-mode . flycheck-rust-setup))

Scala

(use-package scala-mode
  :interpreter ("scala" . scala-mode))

Terraform

(use-package terraform-mode)

Thrift

(use-package thrift)

Tree-sitter

(use-package tree-sitter-langs)

Web

(use-package web-mode
  :config
  (setq web-mode-markup-indent-offset 2)
  (setq web-mode-attr-indent-offset 2)
  (setq web-mode-css-indent-offset 2)
  (setq web-mode-code-indent-offset 2)
  (setq web-mode-enable-auto-pairing t)
  (setq web-mode-enable-css-colorization t)
  (add-hook 'before-save-hook 'delete-trailing-whitespace nil 'local)
  :mode ("\\.html?\\'" "\\.erb\\'" "\\.hbs\\'"
         "\\.json\\'" "\\.s?css\\'" "\\.less\\'" "\\.sass\\'"))

YAML

(use-package yaml-mode
  :config
  (add-hook 'yaml-mode-hook 'highlight-80+-mode))

Zig

(use-package zig-mode
  :config
  (add-hook 'zig-mode-hook #'lsp))