;;; init --- user init file -*- lexical-binding: t; -*-
(eval '(setq inhibit-startup-echo-area-message "alan"))
(defvar default-file-name-handler-alist file-name-handler-alist)
(setq file-name-handler-alist nil
gc-cons-threshold most-positive-fixnum
gc-cons-percentage 0.6
read-process-output-max (* 4 1024 1024))
(defun set-max-gc-cons ()
(setq gc-cons-threshold most-positive-fixnum))
(defun set-default-gc-cons ()
(setq gc-cons-threshold (* 16 1024 1024)
gc-cons-percentage 0.1))
(add-hook 'minibuffer-setup-hook #'set-max-gc-cons)
(add-hook 'minibuffer-exit-hook #'set-default-gc-cons)
(add-hook 'after-init-hook #'set-default-gc-cons)
(defun restore-file-name-handler-alist ()
(setq file-name-handler-alist default-file-name-handler-alist))
(add-hook 'emacs-startup-hook #'restore-file-name-handler-alist)
(defun maybe-start-server ()
(require 'server)
(unless (server-running-p)
(server-start)))
(add-hook 'after-init-hook #'maybe-start-server)
(eval-when-compile (require 'fringe-helper))
(defun reload-user-init-file ()
"Reload init file."
(interactive)
(load-file user-init-file))
(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t)
(run-with-idle-timer 300 nil #'package-refresh-contents)
(setq use-package-enable-imenu-support t)
(require 'use-package)
(setq use-package-always-demand (daemonp)
use-package-compute-statistics t)
(defmacro quietly (&rest body)
`(let ((inhibit-message t))
,@body))
(defun quiet (original-function &rest args)
(quietly (apply original-function args)))
(use-package benchmark-init
:config (progn
(add-hook 'after-init-hook #'benchmark-init/deactivate 99)))
;;; Customize
(setq custom-file (expand-file-name "custom.el" user-emacs-directory))
(load custom-file :noerror :nomessage)
(use-package crux
:defer 1)
(use-package general
:functions (general-unbind general-define-key)
:config (progn
(general-override-mode +1)
(when (eq system-type 'darwin)
(general-unbind "s-x"))))
;;; Styles
;; I prefer an always-visible cursor. Feels less distracting.
(blink-cursor-mode -1)
(setq-default truncate-lines t)
;; Don't ring the bell
(setq ring-bell-function #'ignore)
(use-package doom-themes
:if (or (daemonp)
window-system)
:config (progn
(doom-themes-org-config)))
(let ((light-mode-theme 'doom-one-light)
(dark-mode-theme 'doom-one))
(load-theme light-mode-theme :noconfirm :noenable)
(load-theme dark-mode-theme :noconfirm :noenable)
(defun my/switch-theme-variant (mode)
(interactive (list
(intern (completing-read "Make it: " '("light" "dark") nil t))))
(cond
((eq mode 'dark)
(disable-theme light-mode-theme)
(enable-theme dark-mode-theme))
((eq mode 'light)
(disable-theme dark-mode-theme)
(enable-theme light-mode-theme)))
(modify-all-frames-parameters '((ns-appearance mode))))
(defun my/system-appearance ()
(if (and (eq system-type 'darwin)
(string-equal
"Dark"
(string-trim-right (shell-command-to-string "defaults read -g AppleInterfaceStyle"))))
'dark
'light))
(defun my/match-theme-system-appearance ()
(interactive)
(my/switch-theme-variant (my/system-appearance)))
(my/switch-theme-variant (my/system-appearance))
(use-package kqueue
:if (eq window-system 'ns)
:config (progn
(defun my/gp-notify-callback (event)
(let* ((filename (elt event 2))
(basename (file-name-nondirectory filename)))
(when (string-equal basename ".GlobalPreferences.plist")
(my/match-theme-system-appearance))))
(setq my/kq-watcher-global-preferences
(kqueue-add-watch (expand-file-name "~/Library/Preferences/")
'(write)
#'my/gp-notify-callback)))))
(use-package exec-path-from-shell
:if (eq system-type 'darwin)
:config (progn
(exec-path-from-shell-initialize)))
(global-set-key (kbd "") 'ignore)
(global-set-key (kbd "") 'ignore)
(global-set-key (kbd "") 'ignore)
(setq font-lock-maximum-decoration '((t . 1))
jit-lock-stealth-time 1.25
jit-lock-stealth-nice 0.5
jit-lock-chunk-size 4096)
(context-menu-mode +1)
;;; Chrome
(column-number-mode -1)
(line-number-mode -1)
(use-package nerd-icons
:config (progn
(setq nerd-icons-color-icons t
nerd-icons-scale-factor 1.3)))
(use-package doom-modeline
:hook (emacs-startup . doom-modeline-mode)
:config (progn
(setq doom-modeline-buffer-file-name-style 'relative-from-project
doom-modeline-buffer-encoding 'nondefault
doom-modeline-buffer-modification-icon nil
doom-modeline-check-simple-format t
doom-modeline-percent-position nil
doom-modeline-project-detection 'project
doom-modeline-vcs-max-length 24
doom-modeline-env-version nil
doom-modeline-height 28)
(let ((foreground (face-attribute 'font-lock-comment-face :foreground)))
(set-face-attribute 'doom-modeline-buffer-modified nil :foreground foreground))))
(when (eq system-type 'darwin)
(add-to-list 'default-frame-alist '(ns-transparent-titlebar . t))
(add-to-list 'default-frame-alist '(ns-appearance . 'light))
(setq ns-use-srgb-colorspace nil))
(add-to-list 'default-frame-alist '(width . 100))
(add-to-list 'default-frame-alist '(height . 40))
(setq-default display-line-numbers 'relative
display-line-numbers-widen t
display-line-numbers-width 3)
(defun turn-off-display-line-numbers-mode ()
(interactive)
(display-line-numbers-mode -1))
(defun turn-on-display-line-numbers-mode ()
(interactive)
(display-line-numbers-mode (default-value 'display-line-numbers)))
(setq frame-resize-pixelwise t
window-resize-pixelwise t
display-buffer-alist `(("\\*\\(?:shell\\|compilation\\)\\*" display-buffer-in-side-window
(side . bottom) (slot . 0) (preserve-size . (nil . t))
(no-other-window . t) (no-delete-other-windows . t))))
(use-package buffer-terminator
:defer 60
:config (progn
(buffer-terminator-mode +1)
(setf (alist-get 'kill-buffer-major-modes buffer-terminator-rules-alist)
'(magit-diff-mode
magit-log-mode
magit-process-mode
magit-revision-mode
grep-mode
emacs-lisp-compilation-mode
xref--xref-buffer-mode
helpful-mode)
(alist-get 'kill-buffer-name-regexp buffer-terminator-rules-alist)
(rx
(seq string-start
"*"
(or "EGLOT "
"envrc"
"helpful"
"Async-native-compliation")
(zero-or-more (not "*"))
"*"
string-end)))
(setq buffer-terminator-verbose nil)))
(defun noct-relative ()
"Show relative line numbers."
(when display-line-numbers
(setq-local display-line-numbers 'relative)))
(defun noct-absolute ()
"Show absolute line numbers."
(when display-line-numbers
(setq-local display-line-numbers t)))
(add-hook 'evil-insert-state-entry-hook #'noct-absolute)
(add-hook 'evil-insert-state-exit-hook #'noct-relative)
(use-package ultra-scroll
:defer 1
:config (progn
(setq scroll-margin 0
scroll-conservatively 101)
(ultra-scroll-mode +1)))
;;; Encoding
(setq-default bidi-paragraph-direction 'left-to-right
bidi-display-reordering 'left-to-right)
(setq bidi-inhibit-bpa t
require-final-newline t)
(add-to-list 'auto-coding-alist '("prescient-save\\.el\\'" . utf-8-emacs))
;;; Dates & Times
(defvar calendar-week-start-day 1)
(defvar calendar-date-style 'iso)
(defun insert-date (prefix)
"Insert the current date.
With PREFIX, use British format.
With two prefix arguments, write out the day and month name."
(interactive "P")
(let ((format (cond
((not prefix) "%Y-%m-%d")
((equal prefix '(4)) "%d/%m/%Y")
((equal prefix '(16)) "%A, %d %B %Y"))))
(insert (format-time-string format))))
(defun insert-datetime (prefix)
"Insert current date and time. With PREFIX, use ISO8601 format."
(interactive "P")
(let ((format (cond
((not prefix) "%Y-%m-%d %H:%M:%S")
((equal prefix '(4)) "%Y-%m-%dT%H:%M:%SZ"))))
(insert (format-time-string format))))
;;; Keybindings
(defun prot/keyboard-quit-dwim ()
"Do-What-I-Mean behaviour for a general `keyboard-quit'.
The generic `keyboard-quit' does not do the expected thing when
the minibuffer is open. Whereas we want it to close the
minibuffer, even without explicitly focusing it.
The DWIM behaviour of this command is as follows:
- When the region is active, disable it.
- When a minibuffer is open, but not focused, close the minibuffer.
- When the Completions buffer is selected, close it.
- In every other case use the regular `keyboard-quit'."
(interactive)
(cond
((region-active-p)
(keyboard-quit))
((derived-mode-p 'completion-list-mode)
(delete-completion-window))
((> (minibuffer-depth) 0)
(abort-recursive-edit))
(t
(keyboard-quit))))
(define-key global-map (kbd "C-g") #'prot/keyboard-quit-dwim)
(when (eq system-type 'darwin)
(setq mac-option-modifier 'meta
mac-right-option-modifier 'none
mac-control-modifier 'control
mac-right-control-modifier 'left
mac-command-modifier 'super
mac-right-command-modifier 'left
mac-function-modifier 'hyper)
(define-key global-map (kbd "s-w") #'kill-current-buffer)
(define-key global-map (kbd "s-k") #'kill-current-buffer :remove))
(use-package avy
:defer 2
:config (setq avy-all-windows nil))
(use-package ace-link
:after avy
:commands (ace-link-setup-default)
:config (ace-link-setup-default))
;; Popup keybindings following a prefix automatically.
(use-package which-key
:defer 5
:config (progn
(which-key-mode +1)
(which-key-setup-side-window-right-bottom)))
;;; Minibuffer
(setq enable-recursive-minibuffers t
save-silently t
uniquify-buffer-name-style 'forward
read-extended-command-predicate #'command-completion-default-include-p)
(minibuffer-depth-indicate-mode t)
(use-package hydra
:defer 2)
(use-package recentf
:defer 1
:custom (recentf-auto-cleanup 1800)
:config (progn
(quietly (recentf-mode +1))
(advice-add 'recentf-cleanup :around #'quiet)))
(use-package savehist
:init (savehist-mode +1)
:config (progn
(add-to-list 'savehist-additional-variables 'command-history)))
(use-package persist-state
:defer 1
:ghook ('server-mode-hook #'persist-state-mode))
(use-package vertico
:config (progn
(vertico-mode +1)))
(use-package prescient
:defer 1
:config (progn
(add-to-list 'completion-styles 'prescient)
(setq prescient-history-length 10000)
(prescient-persist-mode +1)))
(use-package vertico-prescient
:after prescient
:config (progn
(vertico-prescient-mode +1)))
(use-package marginalia
:general (:keymaps 'minibuffer-local-map
"M-A" #'marginalia-cycle)
:init (marginalia-mode +1))
(setq completion-ignore-case t
read-buffer-completion-ignore-case t
read-file-name-completion-ignore-case t
completion-styles '(flex substring basic)
completion-category-overrides '((file (styles basic partial-completion))))
(use-package consult
:general ([remap isearch-forward] #'consult-line
[remap isearch-backward] #'consult-line
[remap project-switch-to-buffer] #'consult-project-buffer
[remap project-search] #'consult-ripgrep)
:config (progn
(setq consult-ripgrep-args
"rg --null --line-buffered --color=never --max-columns=1000 --path-separator / --smart-case --no-heading --with-filename --line-number --search-zip --follow")))
(use-package embark
:general ("C-." #'embark-act
"M-." #'embark-dwim
"C-h B" #'embark-bindings)
:config (progn
(setq embark-prompter #'embark-keymap-prompter)
(add-to-list 'display-buffer-alist
'("\\`\\*Embark Collect \\(Live\\|Completions\\)\\*"
nil
(window-parameters (mode-line-format . none))))))
(use-package embark-consult
:ghook ('consult-preview-at-point-mode-hook #'embark-collect-mode))
(use-package smerge-mode
:after magit
:config
(defhydra unpackaged/smerge-hydra
(:color pink :hint nil :post (smerge-auto-leave))
"
^Move^ ^Keep^ ^Diff^ ^Other^
^^-----------^^-------------------^^---------------------^^-------------
_C-j_: next _b_ase _<_: upper/base _C_ombine
_C-k_: prev _u_pper _=_: upper/lower _s_mart resolve
^^ _l_ower _>_: base/lower _k_ill current
^^ _a_ll _R_efine (words)
^^ _RET_: current _E_diff
"
("C-j" smerge-next)
("C-k" smerge-prev)
("b" smerge-keep-base)
("u" smerge-keep-upper)
("l" smerge-keep-lower)
("a" smerge-keep-all)
("RET" smerge-keep-current)
("\C-m" smerge-keep-current)
("<" smerge-diff-base-upper)
("=" smerge-diff-upper-lower)
(">" smerge-diff-base-lower)
("R" smerge-refine)
("E" smerge-ediff)
("C" smerge-combine-with-next)
("s" smerge-resolve)
("k" smerge-kill-current)
("w" (lambda ()
(interactive)
(save-buffer)
(bury-buffer))
"Save and bury buffer" :color blue)
("q" nil "cancel" :color blue))
:hook (magit-diff-visit-file . (lambda ()
(when (bound-and-true-p smerge-mode)
(unpackaged/smerge-hydra/body)))))
;;; Windows
(defun split-window-properly (&optional window)
(let ((window (or window (selected-window))))
(or (and (window-splittable-p window)
;; Split window vertically.
(with-selected-window window
(split-window-below)))
(and (window-splittable-p window t)
;; Split window horizontally.
(with-selected-window window
(split-window-right))))))
(setq split-window-preferred-function #'split-window-properly
split-height-threshold 20
split-width-threshold 160)
(use-package winner
:after evil
:defer 8
:config (progn
(setq winner-boring-buffers '("*Completions*" "*Help*" "*Apropos*" "*Buffer List*" "*info*" "*Compile-Log*"))
(winner-mode +1))
:general (:keymaps 'evil-window-map
"u" #'winner-undo
"r" #'winner-redo
"C-r" #'winner-redo))
;;; Evil
(eval-and-compile
(defvar evil-want-keybinding nil))
(use-package evil
:demand t
:commands (evil-mode evil-delete-buffer evil-ex-define-cmd)
:config (progn
(setq-default evil-shift-width 2)
(setq evil-mode-line-format nil)
(evil-set-undo-system 'undo-redo)
(add-to-list 'evil-emacs-state-modes 'eshell-mode)
(evil-mode +1))
:general
(:states 'motion
"C-;" #'evil-avy-goto-line
"C-" #'evil-switch-to-windows-last-buffer)
(:states 'normal
";" #'evil-ex)
(:states '(normal)
"g ." #'my/ls-code-actions
"g s" #'consult-imenu
"g S" #'my/ls-consult-symbol))
(add-hook 'c-mode-common-hook ; make b/w/e include underscore as *part* of a word
(lambda () (modify-syntax-entry ?_ "w")))
(use-package evil-anzu
:defer 2
:after evil
:config (global-anzu-mode +1))
(use-package evil-collection
:demand t
:general ( :keymaps 'evil-collection-unimpaired-mode-map
:states 'normal
"[ d" #'evil-collection-unimpaired-previous-error
"] d" #'evil-collection-unimpaired-next-error)
:config (progn
(setq evil-collection-magit-use-y-for-yank nil
evil-collection-corfu-key-themes '(default magic-return))
(general-unbind 'normal magit-mode-map
"")
(evil-collection-init)))
(general-create-definer my-leader-def
:keymaps 'override
:states '(normal motion)
:prefix ",")
(use-package evil-space
:defer 1
:after evil
:config (evil-space-mode +1))
;; this macro was copied from here: https://stackoverflow.com/a/22418983/4921402
(defmacro define-and-bind-quoted-text-object (name key start-regex end-regex)
(let ((inner-name (make-symbol (concat "evil-inner-" name)))
(outer-name (make-symbol (concat "evil-a-" name))))
`(progn
(evil-define-text-object ,inner-name (count &optional beg end type)
(evil-select-paren ,start-regex ,end-regex beg end type count nil))
(evil-define-text-object ,outer-name (count &optional beg end type)
(evil-select-paren ,start-regex ,end-regex beg end type count t))
(define-key evil-inner-text-objects-map ,key #',inner-name)
(define-key evil-outer-text-objects-map ,key #',outer-name))))
(use-package evil-surround
:after evil
:defer 2
:config (progn
(add-hook 'js-base-mode-hook (lambda ()
(define-and-bind-quoted-text-object "slash" "/" "\\/" "\\/")
(push '(?\/ . ("/" . "/")) evil-surround-pairs-alist)))
(add-hook 'emacs-lisp-mode-hook (lambda ()
(push '(?` . ("`" . "'")) evil-surround-pairs-alist)))
(global-evil-surround-mode +1)))
(use-package evil-embrace
:after evil-surround
:ghook ('LaTex-mode-hook #'embrace-LaTeX-mode-hook)
:ghook ('org-mode-hook #'embrace-org-mode-hook)
:ghook ('ruby-base-mode-hook #'embrace-ruby-mode-hook)
:ghook ('emacs-lisp-mode-hook #'embrace-emacs-lisp-mode-hook)
:config (progn
(setq evil-embrace-show-help-p nil)
(push ?\/ evil-embrace-evil-surround-keys)
(evil-embrace-enable-evil-surround-integration)))
(use-package evil-exchange
:after evil
:config (progn
(evil-exchange-cx-install)))
(use-package evil-commentary
:after evil
:defer 2
:config (evil-commentary-mode +1))
(use-package evil-lion
:after evil
:defer 10
:config (progn
(evil-lion-mode +1)))
(use-package evil-matchit
:after evil
:defer 2
:config (progn
(global-evil-matchit-mode +1)))
(use-package evil-quickscope
:after evil
:commands (evil-quickscope-mode)
:ghook ('(magit-mode-hook git-rebase-mode-hook) #'turn-off-evil-quickscope-mode)
:config (global-evil-quickscope-mode +1))
(use-package evil-numbers
:after evil
:general (:states '(normal visual)
"C-a" #'evil-numbers/inc-at-pt
"C-q" #'evil-numbers/dec-at-pt))
(use-package evil-org
:after org
:commands (evil-org-set-key-theme)
:init (progn
(add-hook 'org-agenda-mode-hook #'evil-org-agenda-set-keys))
:ghook ('org-mode-hook #'evil-org-mode)
:gfhook #'evil-org-set-key-theme)
(use-package evil-textobj-tree-sitter
:defer 5
:config (progn
(defun etts/start-of-next-function ()
(interactive)
(evil-textobj-tree-sitter-goto-textobj "function.outer" nil nil))
(defun etts/start-of-prev-function ()
(interactive)
(evil-textobj-tree-sitter-goto-textobj "function.outer" t nil))
(defun etts/end-of-next-function ()
(interactive)
(evil-textobj-tree-sitter-goto-textobj "function.outer" nil t))
(defun etts/end-of-prev-function ()
(interactive)
(evil-textobj-tree-sitter-goto-textobj "function.outer" t t))
(general-define-key :keymaps 'evil-outer-text-objects-map
"f" (evil-textobj-tree-sitter-get-textobj "function.outer")
"a" (evil-textobj-tree-sitter-get-textobj ("conditional.outer" "loop.outer")))
(general-define-key :keymaps 'evil-inner-text-objects-map
"f" (evil-textobj-tree-sitter-get-textobj "function.inner")
"a" (evil-textobj-tree-sitter-get-textobj ("conditional.inner" "loop.inner"))))
:general
(:states 'normal
"]f" #'etts/start-of-next-function
"[f" #'etts/start-of-prev-function
"]F" #'etts/end-of-next-function
"[F" #'etts/end-of-prev-function))
;;; Completion
(use-package corfu
:defer 1
:config (progn
(global-corfu-mode +1)
(setq corfu-auto t
corfu-auto-delay 0.1
corfu-auto-prefix 3
corfu-on-exact-match nil
corfu-preview-current nil
corfu-preselect 'prompt
corfu-on-exact-match 'quit)
(defun corfu-enable-in-minibuffer ()
"Enable Corfu in the minibuffer if `completion-at-point' is bound."
(when (where-is-internal #'completion-at-point (list (current-local-map)))
(setq-local corfu-auto nil) ;; Enable/disable auto completion
(setq-local corfu-echo-delay nil ;; Disable automatic echo and popup
corfu-popupinfo-delay nil)
(corfu-mode 1)))
(add-hook 'minibuffer-setup-hook #'corfu-enable-in-minibuffer)
(add-hook 'eshell-mode-hook (lambda ()
(setq-local corfu-auto nil)
(corfu-mode +1)))))
(use-package cape
:after (corfu)
:general (:states 'insert
"C-x C-f" #'cape-file))
(use-package kind-icon
:after (corfu)
:config (progn
(setq kind-icon-default-face 'corfu-default)
(add-to-list 'corfu-margin-formatters #'kind-icon-margin-formatter)))
(use-package tempel
:general ("M-+" #'tempel-complete ;; Alternative tempel-expand
"M-*" #'tempel-insert
:keymaps 'tempel-mode-map
"" #'tempel-next)
:config (progn
(global-tempel-abbrev-mode +1)))
(use-package tempel-collection
:after tempel)
;;; Documentation
(use-package eldoc
:defer 5
:config (progn
(setq eldoc-idle-delay 0.5
eldoc-echo-area-use-multiline-p nil)
(global-eldoc-mode +1)))
(use-package eldoc-box
:defer t
:general (:states '(normal)
"g h" #'eldoc-box-help-at-point))
(use-package ehelp
:defer 15
:general ([remap help-map] 'electric-help-map))
(use-package helpful
:general (ehelp-map
"k" #'helpful-key
"v" #'helpful-variable
"f" #'helpful-callable))
;;; Files
;;;; Auto-saving
(setq auto-save-default nil
make-backup-files nil
create-lockfiles nil)
;;;; Auto-reloading
(use-package autorevert
:config (progn
(setq auto-revert-verbose nil
auto-revert-use-notify t)
(global-auto-revert-mode 1)))
(setq delete-by-moving-to-trash t)
(defun my/delete-file-and-buffer ()
"Kill the current buffer and deletes the file it is visiting."
(interactive)
(let ((filename (buffer-file-name)))
(when filename
(when (y-or-n-p (format "Are you sure you want to delete %s? " filename))
(delete-file filename delete-by-moving-to-trash)
(message "Deleted file %s" filename)
(kill-buffer)))))
(use-package goto-chg
:defer 1)
;;;; TRAMP
(use-package tramp
:defer 10
:config (progn
(add-to-list 'tramp-default-proxies-alist
'(nil "\\`root\\'" "/ssh:%h:"))
(add-to-list 'tramp-default-proxies-alist
`(,(regexp-quote (system-name)) nil nil))
;; https://coredumped.dev/2025/06/18/making-tramp-go-brrrr./
(setq remote-file-name-inhibit-locks t
remote-file-name-inhibit-auto-save-visited t
tramp-use-scp-direct-remote-copying t
tramp-copy-size-limit 1048576
tramp-default-method "rsync")
(connection-local-set-profile-variables 'remote-direct-async-process
'((tramp-direct-async-process . t)))
(connection-local-set-profiles '(:application tramp :machine "linde")
'remote-direct-async-process)
(setq magit-tramp-pipe-stty-settings 'pty)
(with-eval-after-load 'compile
(remove-hook 'compilation-mode-hook #'tramp-compile-disable-ssh-controlmaster-options))))
(use-package ssh-deploy
:config (progn
(ssh-deploy-line-mode +1)
(ssh-deploy-add-find-file-hook)
(ssh-deploy-add-after-save-hook)))
;;; Directories
(setq dired-dwim-target t
dired-recursive-copies 'top
dired-listing-switches "-alh"
dired-kill-when-opening-new-dired-buffer t
dired-recursive-deletes (if delete-by-moving-to-trash
'always
'top))
(add-hook 'dired-mode-hook
(lambda ()
(dired-hide-details-mode)))
;;; Shells
(use-package eshell
:defer 5
:commands (eshell)
:functions (eshell/pwd)
:gfhook #'turn-off-display-line-numbers-mode
:general (:keymaps 'eshell-command-map
"C-r" #'eshell-history-backwards
"C-s" #'eshell-history-forwards)
:init (progn
(with-eval-after-load 'evil-ex
(evil-ex-define-cmd "esh[ell]" #'eshell)))
:config (progn
(eshell-load-modules eshell-modules-list)
(setq eshell-prompt-function (lambda ()
(concat (eshell/pwd) "\n$ "))
eshell-prompt-regexp "^[$][[:blank:]]"
eshell-cmpl-cycle-completions nil)
(add-hook 'eshell-exit-hook (lambda ()
(when (window-deletable-p)
(delete-window))))))
(use-package eshell-toggle
:commands (eshell-toggle)
:general ("C-`" #'eshell-toggle)
:config (progn
(setq eshell-toggle-find-project-root-package 'project)))
(declare-function eshell-push-command "esh-buf-stack" (CMD))
(defun my-bind-esh-push ()
(general-define-key
:states '(normal insert)
:keymaps 'local
"M-q" #'eshell-push-command))
(use-package esh-buf-stack
:after (eshell)
:ghook ('eshell-mode-hook #'my-bind-esh-push)
:config (setup-eshell-buf-stack))
(use-package esh-help
:after (eshell)
:config (setup-esh-help-eldoc))
(use-package eshell-fringe-status
:after eshell
:ghook '(eshell-mode-hook))
(use-package eshell-up
:after (eshell))
(use-package shell
:defer t
:general (:keymaps 'shell-mode-map
"C-d" #'comint-delchar-or-maybe-eof))
(use-package comint
:defer t
:general (:keymaps 'comint-mode-map
"C-c C-l" #'counsel-shell-history))
;;; Editing
(setq-default tab-always-indent 'complete
indent-tabs-mode nil
tab-width 4)
(if (fboundp 'kill-ring-deindent-mode)
(kill-ring-deindent-mode +1))
(electric-pair-mode +1)
(use-package ws-butler
:ghook ('prog-mode-hook))
(use-package dtrt-indent
:commands (dtrt-indent-mode))
(use-package rainbow-mode
:ghook ('(css-mode-hook
conf-xdefaults-mode-hook)
#'rainbow-mode))
(use-package expand-region
:general (:states 'visual
"SPC" #'er/expand-region))
;;; Major modes
;;;; tree-sitter
(use-package treesit-auto
:config (progn
(global-treesit-auto-mode)
(treesit-auto-add-to-auto-mode-alist)))
;;;; golang
(with-eval-after-load 'project
(add-to-list 'project-vc-extra-root-markers "go.mod"))
(setq-default go-ts-mode-indent-offset 2)
(use-package templ-ts-mode
:gfhook #'eglot-format-before-save-mode
:defer t
:config (progn
(setq-default go-ts-mode-indent-offset 2)))
;;;; nim
(use-package nim-mode
:defer t
:config (progn
(add-to-list 'eglot-server-programs
'(nim-mode "nimlsp"))))
;;;; js
(setq js-enabled-frameworks '(javascript))
(add-to-list 'auto-mode-alist '("\\.[cm]js\\'" . js-ts-mode))
(add-to-list 'auto-mode-alist '("\\.lock" . json-ts-mode))
;;;; typescript
(use-package astro-ts-mode
:mode (("\\.astro\\'" . astro-ts-mode)))
(autoload 'ansi-color-apply-on-region "ansi-color")
(defun colourise-compilation-buffer ()
(ansi-color-apply-on-region compilation-filter-start (point-max)))
(add-hook 'compilation-filter-hook #'colourise-compilation-buffer)
;;;; shell
(general-add-hook '(sh-mode-hook bash-ts-mode-hook fish-mode-hook)
(lambda ()
(general-add-hook 'after-save-hook
#'executable-make-buffer-file-executable-if-script-p :append :local)))
(add-to-list 'auto-mode-alist '("\\.env\\'" . conf-unix-mode))
(add-to-list 'auto-mode-alist '("\\.zsh\\'" . shell-script-mode))
(add-to-list 'auto-mode-alist '("zshenv\\'" . shell-script-mode))
(add-hook 'shell-mode-hook 'ansi-color-for-comint-mode-on)
(when (eq system-type 'gnu/linux)
(setenv "SSH_AUTH_SOCK" (expand-file-name "ssh-agent" (getenv "XDG_RUNTIME_DIR"))))
(use-package fish-mode
:mode (("\\.fish\\'" . fish-mode))
:config (progn
(setq fish-enable-auto-indent t)))
;;;; nix
(with-eval-after-load 'nix-mode
(setq nix-mode-use-smie t
nix-indent-function #'smie-indent-line))
(use-package nix-ts-mode
:mode (("\\.nix\\'" . nix-ts-mode)))
(use-package nix-update
:commands (nix-update-fetch))
;;;; gitlab-ci.yml
(with-eval-after-load 'git-gutter-fringe
(fringe-helper-define 'flycheck-fringe-bitmap-double-arrow '(center repeated)
"XXX....."))
(use-package gitlab-ci-mode-flycheck
:ghook ('gitlab-ci-mode-hook (list #'gitlab-ci-mode-flycheck-enable
#'flycheck-mode)))
;;;; *ignore
(use-package gitignore-mode
:mode ((".dockerignore\\'" . gitignore-mode)))
;;;; gitolite
(use-package gl-conf-mode
:defer t)
;;;; lisps
(use-package racket-mode
:ghook ('racket-mode-hook #'racket-xp-mode))
(use-package clojure-mode
:defer t)
(use-package cider
:defer t
:after clojure-mode)
(use-package rainbow-delimiters
:ghook '(clojure-mode-hook
emacs-lisp-mode-hook))
(use-package lispyville
:ghook '(emacs-lisp-mode-hook
clojure-mode-hook
racket-mode-hook))
;;;; org
(use-package org
:defer 10
:config (progn
(setq org-ellipsis "…"
org-modules nil
org-directory "~/Documents/org")))
;;;; web modes (html)
(use-package css-mode
:defer t)
(use-package web-mode
:mode (("\\.html?.erb\\'" . web-mode)
("\\.gotmpl\\'" . web-mode)
("\\.tmpl\\'" . web-mode))
:config (setq web-mode-enable-auto-pairing nil
web-mode-style-padding 2
web-mode-script-padding 2
web-mode-engines-alist '(("go" . "\\.tmpl\\'")
("go" . "\\.gotmpl\\'"))))
(use-package emmet-mode
:ghook '(web-mode-hook sgml-mode-hook templ-ts-mode-hook))
;;; IDE features
(fringe-helper-define 'left-vertical-bar '(center repeated)
"XXX.....")
(use-package flymake
:defer 5
:config (setq flymake-error-bitmap '(left-vertical-bar compilation-error)
flymake-warning-bitmap '(left-vertical-bar compilation-warning)))
(use-package flymake-popon
:after flymake
:ghook ('flymake-mode-hook #'flymake-popon-mode))
(use-package flycheck
:defer t
:config (progn
(setq flycheck-check-syntax-automatically '(save idle-buffer-switch mode-enabled)
flycheck-highlighting-mode 'sexps)
(flycheck-define-error-level 'error
:severity 100
:compilation-level 2
:overlay-category 'flycheck-error-overlay
:fringe-bitmap 'left-vertical-bar
:fringe-face 'flycheck-fringe-error
:error-list-face 'flycheck-error-list-error)
(flycheck-define-error-level 'warning
:severity 10
:compilation-level 1
:overlay-category 'flycheck-warning-overlay
:fringe-bitmap 'left-vertical-bar
:fringe-face 'flycheck-fringe-warning
:warning-list-face 'flycheck-error-list-warning)
(flycheck-define-error-level 'info
:severity -10
:compilation-level 0
:overlay-category 'flycheck-info-overlay
:fringe-bitmap 'left-vertical-bar
:fringe-face 'flycheck-fringe-info
:info-list-face 'flycheck-error-list-info)))
;;; Projects
(use-package project
:bind (("s-p" . #'project-find-file)
("s-o" . #'project-switch-project))
:general (:keymaps 'project-prefix-map
"s" #'project-search)
:config (progn
(setq project-switch-commands #'magit-project-status)
(with-eval-after-load 'evil-ex
(evil-ex-define-cmd "pesh[ell]" #'project-eshell)
(evil-ex-define-cmd "pb" #'project-switch-to-buffer)
(evil-ex-define-cmd "psw[itch]" #'project-switch-project))))
(defun my/project-or-not-find-file (&optional include-all)
(interactive "P")
(if (project-current nil)
(call-interactively #'project-find-file)
(call-interactively #'find-file)))
(defun ap/consult-ghq-switch-project (dir)
"Append a slash to avoid project.el remembering two different
paths for the same project."
(interactive)
(project-switch-project (if (string-suffix-p "/" dir)
dir
(concat dir "/"))))
(use-package consult-ghq
:defer 5
:general (:keymaps 'project-prefix-map
"o" #'consult-ghq-switch-project)
:config (progn
(setq consult-ghq-grep-function #'consult-grep
consult-ghq-find-function #'consult-find
consult-ghq-switch-project-function #'ap/consult-ghq-switch-project)))
(use-package envrc
:defer 2
:config (progn
(setq envrc-show-summary-in-minibuffer nil)
(envrc-global-mode)))
(use-package magit
:defer 5
:commands (magit-status magit-dispatch)
:general ([remap project-vc-dir] #'magit-project-status)
(:keymaps 'project-prefix-map "m" #'magit-project-status)
(:keymaps 'magit-mode-map "z" nil)
(:keymaps 'magit-status-mode-map "z" #'magit-stash)
(:keymaps 'magit-section-mode-map "C-" nil)
:config (progn
(add-to-list 'magit-git-environment "NO_COLOR=1")
(with-eval-after-load 'evil-ex
(evil-ex-define-cmd "mg" #'magit-status))
(setq magit-section-visibility-indicator nil
magit-diff-refine-hunk t
magit-auto-revert-mode nil
magit-auto-revert-immediately nil ; unnecessary when global-auto-revert-mode is enabled
magit-display-buffer-function #'magit-display-buffer-fullcolumn-most-v1)
(remove-hook 'magit-status-sections-hook 'magit-insert-tags-header)
(remove-hook 'magit-section-highlight-hook 'magit-section-highlight)
(remove-hook 'magit-section-highlight-hook 'magit-section-highlight-selection)
(remove-hook 'magit-section-highlight-hook 'magit-diff-highlight)
(require 'magit-extras)))
(use-package magit-todos
:defer 10
:config (progn
(magit-todos-mode +1)
(add-to-list 'magit-todos-exclude-globs "*.map")))
(use-package difftastic
:defer 5)
(use-package difftastic-bindings
:after magit
:config (progn
(difftastic-bindings-mode +1)))
(use-package git-gutter-fringe
:defer 5
:config (progn
(add-hook 'magit-post-refresh-hook
#'git-gutter:update-all-windows)
;; places the git gutter outside the margins.
(setq-default fringes-outside-margins nil)
;; thin fringe bitmaps
(fringe-helper-define 'git-gutter-fr:added '(center repeated)
".XXX....")
(fringe-helper-define 'git-gutter-fr:modified '(center repeated)
".XXX....")
(fringe-helper-define 'git-gutter-fr:deleted '(center repeated)
".XXX....")
(setq git-gutter-fr:side 'left-fringe)
(global-git-gutter-mode 1)))
(use-package vc-msg
:defer 30)
(use-package git-timemachine
:commands (git-timemachine))
(use-package editorconfig
:defer 2
:config (progn
(editorconfig-mode +1)
(setq editorconfig-lisp-use-default-indent t)
(setf (alist-get 'templ-ts-mode editorconfig-indentation-alist)
'go-ts-mode-indent-offset)))
(setq-default ispell-dictionary "en_GB-ise-w_accents")
(setq ispell-extra-args '("--sug-mode=ultra" "--camel-case"))
(use-package jinx-mode
:defer 10
:ghook 'text-mode-hook
:general ([remap ispell-word] #'jinx-correct-word
[remap evil-prev-flyspell-error] #'jinx-previous
[remap evil-next-flyspell-error] #'jinx-next)
:config (progn
(advice-add 'jinx--load-dicts :after (lambda ()
(unless jinx--dicts
(global-jinx-mode -1))))))
(use-package feature-mode
:defer t
:config (progn
(setq feature-cucumber-command "cucumber-js {options} \"{feature}\"")))
(defvaralias 'typescript-ts-mode-hook 'typescript-mode-hook)
(defvaralias 'dockerfile-ts-mode-hook 'dockerfile-mode-hook)
(defvaralias 'yaml-ts-mode-hook 'yaml-mode-hook)
(defvaralias 'go-ts-mode-hook 'go-mode-hook)
(defvaralias 'nix-ts-mode-hook 'nix-mode-hook)
(use-package consult-lsp
:commands (consult-lsp-symbols
consult-lsp-diagnostics))
(define-minor-mode eglot-format-before-save-mode
"Whether to ask the LSP to format the buffer before saving"
:init-val nil
(if eglot-format-before-save-mode
(progn
(add-hook 'before-save-hook #'eglot-format-buffer nil 'local)
(when (bound-and-true-p apheleia-mode)
(apheleia-mode -1)))
(remove-hook 'before-save-hook #'eglot-format-buffer 'local)))
(use-package eglot
:defer 3
:general (:states 'normal :keymaps 'eglot-mode-map
"gr" #'xref-find-references
"C-t" #'xref-go-back)
:ghook ('(typescript-mode-hook
dockerfile-mode-hook
yaml-mode-hook
js-base-mode-hook
fish-mode-hook
css-base-mode-hook
lua-mode-hook
markdown-mode-hook
nim-mode-hook
html-mode-hook
nix-mode-hook
templ-ts-mode-hook
toml-ts-mode-hook
just-ts-mode-hook
haskell-mode-hook)
#'eglot-ensure)
:config (progn
(when (assoc 'nix-mode eglot-server-programs)
(setf (car (assoc 'nix-mode eglot-server-programs)) '(nix-mode nix-ts-mode)))
(nconc eglot-server-programs '((toml-ts-mode "taplo" "lsp" "stdio")
(just-ts-mode "just-lsp")
(fish-mode "fish-lsp" "start")))
(advice-add 'eglot--message :before-until (lambda (formatstring &rest rest)
(s-starts-with-p "Connected!" formatstring)))
(defun my/setup-eglot-eldoc ()
(push 'flymake-eldoc-function eldoc-documentation-functions))
(add-hook 'eglot-managed-mode-hook 'my/setup-eglot-eldoc)
(setq-default eglot-workspace-configuration
'( :yaml (:keyOrdering :json-false)
:nil (:nix (:flake (:autoArchive t)))
:gopls ( :staticcheck t
:usePlaceholders t)))
(defun my/eglot-capf ()
(setq-local completion-at-point-functions
(list (cape-capf-super
#'eglot-completion-at-point
#'tempel-expand
#'cape-file))))
(add-hook 'eglot-managed-mode-hook #'my/eglot-capf)))
(use-package eglot-booster
:after eglot
:config (progn
(eglot-booster-mode +1)))
(use-package eglot-tempel
:after eglot
:config (progn
(eglot-tempel-mode +1)))
(use-package consult-eglot
:commands (consult-eglot-symbols)
:after eglot)
(use-package lsp-mode
:defer 3
:ghook ('(go-mode-hook)
#'lsp-deferred)
('lsp-mode-hook #'lsp-enable-which-key-integration)
('lsp-completion-mode-hook #'my/lsp-mode-setup-completion)
:general (:states 'normal :keymaps 'lsp-mode-map
"gr" #'xref-find-references
"C-t" #'xref-go-back)
:config (progn
(setq lsp-auto-guess-root t
lsp-auto-execute-action nil
lsp-headerline-breadcrumb-enable nil
lsp-enable-suggest-server-download nil
lsp-modeline-diagnostics-enable nil
lsp-completion-provider :none ; value `:capf' is actually for company :(
lsp-diagnostics-provider t ; this means prefer flymake over flycheck, why‽
)
(defun lsp-booster--advice-json-parse (old-fn &rest args)
"Try to parse bytecode instead of json."
(or
(when (equal (following-char) ?#)
(let ((bytecode (read (current-buffer))))
(when (byte-code-function-p bytecode)
(funcall bytecode))))
(apply old-fn args)))
(advice-add (if (progn (require 'json)
(fboundp 'json-parse-buffer))
'json-parse-buffer
'json-read)
:around
#'lsp-booster--advice-json-parse)
(defun lsp-booster--advice-final-command (old-fn cmd &optional test?)
"Prepend emacs-lsp-booster command to lsp CMD."
(let ((orig-result (funcall old-fn cmd test?)))
(if (and (not test?) ;; for check lsp-server-present?
(not (file-remote-p default-directory)) ;; see lsp-resolve-final-command, it would add extra shell wrapper
lsp-use-plists
(not (functionp 'json-rpc-connection)) ;; native json-rpc
(executable-find "emacs-lsp-booster"))
(progn
(when-let ((command-from-exec-path (executable-find (car orig-result)))) ;; resolve command from exec-path (in case not found in $PATH)
(setcar orig-result command-from-exec-path))
(message "Using emacs-lsp-booster for %s!" orig-result)
(cons "emacs-lsp-booster" orig-result))
orig-result)))
(advice-add 'lsp-resolve-final-command :around #'lsp-booster--advice-final-command)
(defun my/lsp-mode-setup-completion ()
(setf (alist-get 'styles (alist-get 'lsp-capf completion-category-defaults))
'(flex)))
(lsp-register-custom-settings
'(("golangci-lint.command"
["golangci-lint" "run" "--output.json.path" "/dev/stdout"])))
(lsp-register-client
(make-lsp-client :new-connection (lsp-stdio-connection
'("golangci-lint-langserver"))
:activation-fn (lsp-activate-on "go")
:language-id "go"
:priority 0
:server-id 'golangci-lint
:add-on? t
:library-folders-fn #'lsp-go--library-default-directories
:initialization-options (lambda ()
(gethash "golangci-lint"
(lsp-configuration-section "golangci-lint")))))))
(use-package yasnippet
:after lsp-mode
:ghook ('lsp-completion-mode-hook #'yas-minor-mode))
(defun my/ls-rename ()
(interactive)
(if lsp-mode
(call-interactively #'lsp-rename)
(call-interactively #'eglot-rename)))
(defun my/ls-consult-symbol ()
(interactive)
(if lsp-mode
(call-interactively #'consult-lsp-symbols)
(call-interactively #'consult-eglot-symbols)))
(defun my/ls-code-actions ()
(interactive)
(call-interactively
(if lsp-mode
#'lsp-execute-code-action
#'eglot-code-actions)))
;;;; Reformat on save
(use-package apheleia
:defer 11
:ghook 'prog-mode-hook
:config (progn
(setf (alist-get 'shfmt apheleia-formatters)
'("shfmt"))
(setq apheleia-formatters
(append apheleia-formatters '((golines "golines")
(taplo "taplo" "format" "-")
(sqlfluff "sqlfluff" "format" "--stdin-filename" filepath "-")
(prettier-gotmpl
"prettier" "--stdin-filepath" filepath
"--parser=go-template" (apheleia-formatters-indent "--use-tabs" "--tab-width")))))
(setf (alist-get 'sql-mode apheleia-mode-alist) 'sqlfluff)
(setq apheleia-mode-alist (append apheleia-mode-alist '((toml-ts-mode . taplo)))))
:init (progn
(apheleia-global-mode +1)))
;;; Take me to my leader
(my-leader-def
"" nil
"`" #'eshell-toggle
"h" '(:keymap ehelp-map :package ehelp)
"w" '(:keymap evil-window-map :package evil)
"x" '(:keymap ctl-x-map)
"c" (general-simulate-key "C-c")
"j" #'my/ls-consult-symbol
"m" #'magit-status
"r" #'my/ls-rename
"q" #'evil-delete-buffer
"p" '(:keymap project-prefix-map :package project)
"v" #'split-window-right
"o" #'other-window
"u" #'universal-argument
";" #'execute-extended-command
"a" #'my/ls-code-actions
"bb" #'consult-buffer
"bx" #'kill-current-buffer
"br" #'revert-buffer
"bk" #'kill-buffer
"dd" #'dired
"D" #'consult-dir
"e" '(:keymap envrc-command-map :package envrc)
"fs" #'save-buffer
"ff" #'my/project-or-not-find-file
"fw" #'write-file
"fd" #'my/delete-file-and-buffer
"fr" #'crux-rename-file-and-buffer
"gm" #'vc-msg-show
"gg" #'magit-dispatch
"gn" #'git-gutter:next-hunk
"gp" #'git-gutter:previous-hunk
"gi" #'git-gutter:popup-hunk
"gs" #'git-gutter:stage-hunk
"go" #'git-gutter:revert-hunk
"gt" #'git-timemachine
"gl" #'magit-log-buffer-file
"bz" #'bury-buffer
"iu" #'insert-char
"xe" #'eval-last-sexp
"xx" #'eval-defun
"xi" #'consult-imenu
"z" '(:keymap ssh-deploy-prefix-map :package ssh-deploy))
(let ((mail-config (expand-file-name "mail.el" user-emacs-directory)))
(if (file-readable-p mail-config)
(load mail-config :noerror :nomessage)))
;; # Local Variables:
;; # flycheck-disabled-checkers: 'emacs-lisp-checkdoc
;; # End:
user/settings/emacs/init.el (view raw)