I use Emacs for almost all general computing needs besides web browsing. I used to maintain its configuration separately from my machine configurations, but since I am moving to a more literate approach, it makes sense to move it here.
emacs-overlay.url = "github:nix-community/emacs-overlay";
Pass-in the input as a named argument to outputs
, so that it is available in modules:
I am wiring up my Emacs distribution using the withPackages
function
described in the Nixpkgs Manual here. Currently I am experimenting
retargeting user-emacs-directory
to this repository, so I don’t need
to rebuild Emacs
every time I want to adjust its configuration.
{
vlaci-emacs =
let
inherit (pkgs) lib;
pwd = builtins.getEnv "PWD";
initDirectory = "${pwd}/out/emacs.d";
dicts = with pkgs.hunspellDicts; [ # See jinx below
hu-hu
en-us-large
];
dictSearchPath = lib.makeSearchPath "share/hunspell" dicts;
emacsWithPackages = inputs.emacs-overlay.lib.${pkgs.system}.emacsPackagesFor pkgs.emacs30-pgtk;
emacs =
(emacsWithPackages.overrideScope (
lib.composeManyExtensions [
(final: prev: {
mkPackage =
{
pname,
src,
files ? [ "*.el" ],
...
}@args:
let
files' =
let
list = lib.concatStringsSep " " (map (f: ''"${lib.escape [ ''"'' ] f}"'') files);
in
"(${list})";
version =
let
ver = src.lastModifiedDate or inputs.self.lastModifiedDate;
removeLeadingZeros =
s:
let
s' = lib.removePrefix "0" s;
in
if lib.hasPrefix "0" s' then removeLeadingZeros s' else s';
major = removeLeadingZeros (builtins.substring 0 8 ver);
minor = removeLeadingZeros (builtins.substring 8 6 ver);
in
args.version or "${major}.${minor}";
in
final.melpaBuild (
{
inherit version src;
commit =
src.rev or inputs.self.sourceInfo.rev or inputs.self.sourceInfo.dirtyRev
or "00000000000000000000000000000000";
recipe = pkgs.writeText "recipe" ''
(${pname}
:fetcher git
:url "nohost.nodomain"
:files ${files'})
'';
}
// removeAttrs args [ "files" ]
);
})
<<emacs-package-overrides>>
]
)).withPackages
(
epkgs:
with epkgs;
[
<<emacs-packages>>
]
);
binaries = with pkgs; [
<<emacs-nixpkgs>>
];
in
assert lib.assertMsg (pwd != "") "Use --impure flag for building";
emacs.overrideAttrs (super: {
# instead of relyiong on `package.el` to wire-up autoloads, do it build-time
deps = super.deps.overrideAttrs (
dsuper:
let
genAutoloadsCommand = ''
echo "-- Generating autoloads..."
autoloads=$out/share/emacs/site-lisp/autoloads.el
for pkg in "''${requires[@]}"; do
autoload=("$pkg"/share/emacs/site-lisp/*/*/*-autoloads.el)
if [[ -e "$autoload" ]]; then
cat "$autoload" >> "$autoloads"
fi
done
echo "(load \"''$autoloads\")" >> "$siteStart"
# Byte-compiling improves start-up time only slightly, but costs nothing.
$emacs/bin/emacs --batch -f batch-byte-compile "$autoloads" "$siteStart"
$emacs/bin/emacs --batch \
--eval "(add-to-list 'native-comp-eln-load-path \"$out/share/emacs/native-lisp/\")" \
-f batch-native-compile "$autoloads" "$siteStart"
'';
in
{
buildCommand = ''
${dsuper.buildCommand}
${genAutoloadsCommand}
'';
}
);
buildCommand = ''
${super.buildCommand}
wrapProgram $out/bin/emacs \
--append-flags "--init-directory ${initDirectory}" \
--suffix PATH : ${
with lib;
pipe binaries [
makeBinPath
escapeShellArg
]
} \
--prefix DICPATH : ${lib.escapeShellArg dictSearchPath}
'';
});
}
Lets add it to installed packages:
{ pkgs, ... }:
{
home.packages = [ pkgs.vlaci-emacs ];
}
{
_.persist.allUsers.directories = [ ".cache/emacs" ];
}
Also, initialize the basics:
;; -*- lexical-binding: t; -*-
(setq gc-cons-threshold most-positive-fixnum)
(add-hook 'after-init-hook
`(lambda ()
(setq file-name-handler-alist ',file-name-handler-alist))
0)
(setq file-name-handler-alist nil)
(setq package-enable-at-startup nil
frame-resize-pixelwise t
frame-inhibit-implied-resize t
frame-title-format '("%b")
ring-bell-function 'ignore
use-dialog-box nil
use-file-dialog nil
use-short-answers t
inhibit-splash-screen t
inhibit-startup-screen t
inhibit-x-resources t
inhibit-startup-echo-area-message user-login-name ; read the docstring
inhibit-startup-buffer-menu t)
;; Prevent the glimpse of un-styled Emacs by disabling these UI elements early.
(push '(menu-bar-lines . 0) default-frame-alist)
(push '(tool-bar-lines . 0) default-frame-alist)
(push '(vertical-scroll-bars) default-frame-alist)
(defvar user-cache-directory "~/.cache/emacs/"
"Location where files created by emacs are placed.")
(defun vlaci/in-cache-directory (name)
"Return NAME appended to cache directory"
(expand-file-name name user-cache-directory))
(defvar vlaci/init-directory user-emacs-directory)
(defun vlaci/in-init-directory (name)
"Return NAME appended to init directory"
(expand-file-name name vlaci/init-directory))
(startup-redirect-eln-cache (vlaci/in-cache-directory "eln-cache"))
(setq user-emacs-directory user-cache-directory)
final: prev: {
setup = prev.setup.overrideAttrs (_: {
ignoreCompilationError = true;
});
}
setup
gcmh
;; -*- lexical-binding: t; -*-
;; Use font from Gsettings from /org/gnome/desktop/interface/
;; The keys read are:
;; - ‘font-name’
;; - 'monospace-font-name’
(require 'vlaci-emacs)
(setq font-use-system-font t)
(set-face-attribute 'fixed-pitch nil :height 1.0)
(set-face-attribute 'variable-pitch nil :height 1.0)
(setup (:package gcmh)
(:hook-into on-first-buffer-hook)
(:option gcmh-verbose init-file-debug
gcmh-high-cons-threshold (* 128 1024 1024)))
(setup emacs
(setq user-emacs-directory user-cache-directory)
(:option
custom-file (vlaci/in-cache-directory "custom.el")
auto-save-interval 2400
auto-save-timeout 300
auto-save-list-file-name (vlaci/in-cache-directory "auto-save.lst")
auto-save-file-name-transforms `((".*" ,(vlaci/in-cache-directory "auto-save/") t))
backup-directory-alist `(("." . ,(vlaci/in-cache-directory "backup/")))
backup-by-copying t
version-control t
delete-old-versions t
kept-new-versions 10
kept-old-versions 5)
(make-directory (vlaci/in-cache-directory "auto-save/") :parents)
(load custom-file :no-error-if-file-is-missing))
(setup recentf
(:option recentf-max-saved-items 200
recentf-auto-cleanup 300)
(define-advice recentf-cleanup (:around (fun) silently)
(let ((inhibit-message t)
(message-log-max nil))
(funcall fun)))
(recentf-mode 1))
(setup savehist
(:hook-into on-first-file-hook)
(:option history-length 1000
history-delete-duplicates t
savehist-save-minibuffer-history t
savehist-additional-variables
'(kill-ring ; clipboard
register-alist ; macros
mark-ring global-mark-ring ; marks
search-ring regexp-search-ring))) ; searches
(setup save-place
(:hook-into on-first-file-hook)
(:option save-place-limit 600))
<<init-el>>
My helper package
This contains my to-be compiled code to keep init.el
clean.
mkPackage {
pname = "vlaci-emacs";
version = "1.0";
src = pkgs.writeText "vlaci-emacs.el" ''
;;; vlaci-emacs.el --- local extensions -*- lexical-binding: t; -*-
<<vlaci-emacs>>
(provide 'vlaci-emacs)
'';
packageRequires = [
<<vlaci-emacs-requires>>
];
}
Help
(setup emacs
(:option help-window-keep-selected t)) ;; navigating to e.g. source from help window reuses said window
(setup which-key
(:hook-into on-first-input-hook)
(:with-feature embark
(:when-loaded
(defun embark-which-key-indicator ()
"An embark indicator that displays keymaps using which-key.
The which-key help message will show the type and value of the
current target followed by an ellipsis if there are further
targets."
(lambda (&optional keymap targets prefix)
(if (null keymap)
(which-key--hide-popup-ignore-command)
(which-key--show-keymap
(if (eq (plist-get (car targets) :type) 'embark-become)
"Become"
(format "Act on %s '%s'%s"
(plist-get (car targets) :type)
(embark--truncate-target (plist-get (car targets) :target))
(if (cdr targets) "…" "")))
(if prefix
(pcase (lookup-key keymap prefix 'accept-default)
((and (pred keymapp) km) km)
(_ (key-binding prefix 'accept-default)))
keymap)
nil nil t (lambda (binding)
(not (string-suffix-p "-argument" (cdr binding))))))))
(setq embark-indicators
'(embark-which-key-indicator
embark-highlight-indicator
embark-isearch-highlight-indicator))
(defun embark-hide-which-key-indicator (fn &rest args)
"Hide the which-key indicator immediately when using the completing-read prompter."
(which-key--hide-popup-ignore-command)
(let ((embark-indicators
(remq #'embark-which-key-indicator embark-indicators)))
(apply fn args)))
(advice-add #'embark-completing-read-prompter
:around #'embark-hide-which-key-indicator))))
More hooks
on
(setup (:package on)
(:require on))
UI
auto-dark
spacious-padding
ef-themes
doom-modeline
(setup (:package doom-modeline auto-dark spacious-padding)
(:option spacious-padding-subtle-mode-line t
spacious-padding-widths
'( :internal-border-width 15
:header-line-width 4
:mode-line-width 6
:tab-width 4
:right-divider-width 1
:scroll-bar-width 8
:fringe-width 8)
auto-dark-themes '((modus-vivendi-tinted) (modus-operandi-tinted)))
(defun vlaci--load-theme-h ()
(require-theme 'modus-themes)
(setq modus-themes-italic-constructs t
modus-themes-bold-constructs t
modus-themes-prompts '(background)
modus-themes-mixed-fonts nil
modus-themes-org-blocks 'gray-background
modus-themes-headings '((0 . (2.0))
(1 . (rainbow background overline 1.5))
(2 . (background overline 1.4))
(3 . (background overline 1.3))
(4 . (background overline 1.2))
(5 . (overline 1.2))
(t . (no-bold 1.1)))
modus-themes-common-palette-overrides
`(,@modus-themes-preset-overrides-faint
(builtin magenta)
(comment fg-dim)
(constant magenta-cooler)
(docstring magenta-faint)
(docmarkup green-faint)
(fnname magenta-warmer)
(keyword cyan)
(preprocessor cyan-cooler)
(string red-cooler)
(type magenta-cooler)
(variable blue-warmer)
(rx-construct magenta-warmer)
(rx-backslash blue-cooler)))
(load-theme 'modus-operandi-tinted :no-confirm)
(doom-modeline-mode)
(spacious-padding-mode)
(auto-dark-mode))
(:with-function vlaci--load-theme-h
(:hook-into window-setup-hook)))
repeat-help
(setup (:package repeat-help)
(:option repeat-help-popup-type 'which-key)
(:hook-into repeat-mode-hook)
(:hook (defun vlaci/reset-repeat-echo-function-h ()
(setq repeat-echo-function repeat-help--echo-function))))
(setup repeat
(:hook-into on-first-input-hook))
(setup emacs
(:option display-line-numbers-type 'relative
display-line-numbers-width 3
display-line-numbers-widen t
split-width-threshold 170
truncate-lines t
window-combination-resize t))
(setup prog
(:hook #'display-line-numbers-mode))
lin
(setup (:package lin)
(:with-mode lin-global-mode
(:hook-into on-first-buffer-hook)))
Help
helpful
elisp-demos
(setup (:package helpful elisp-demos)
(:option help-window-select t)
(:global
[remap describe-command] #'helpful-command
[remap describe-function] #'helpful-callable
[remap describe-macro] #'helpful-macro
[remap describe-key] #'helpful-key
[remap describe-symbol] #'helpful-symbol
[remap describe-variable] #'helpful-variable)
(:when-loaded
(require 'elisp-demos)
(advice-add 'helpful-update :after #'elisp-demos-advice-helpful-update)))
More fine grained lazy loading
once = { url = "github:emacs-magus/once"; flake = false; };
mkPackage {
pname = "once";
src = inputs.once;
files = [
"*.el"
"once-setup/*.el"
];
packageRequires = [
(setup.overrideAttrs (_: {
ignoreCompilationError = true;
}))
];
}
(setup (:package once)
(:option once-shorthand t)
(:require once once-conditions))
(setup (:package once-setup)
(:require once-setup))
setup
(defvar vlaci-incremental-packages '(t)
"A list of packages to load incrementally after startup. Any large packages
here may cause noticeable pauses, so it's recommended you break them up into
sub-packages. For example, `org' is comprised of many packages, and can be
broken up into:
(vlaci-load-packages-incrementally
'(calendar find-func format-spec org-macs org-compat
org-faces org-entities org-list org-pcomplete org-src
org-footnote org-macro ob org org-clock org-agenda
org-capture))
This is already done by the lang/org module, however.
If you want to disable incremental loading altogether, either remove
`doom-load-packages-incrementally-h' from `emacs-startup-hook' or set
`doom-incremental-first-idle-timer' to nil.")
(defvar vlaci-incremental-first-idle-timer 2.0
"How long (in idle seconds) until incremental loading starts.
Set this to nil to disable incremental loading.")
(defvar vlaci-incremental-idle-timer 0.75
"How long (in idle seconds) in between incrementally loading packages.")
(defvar vlaci-incremental-load-immediately nil
;; (daemonp)
"If non-nil, load all incrementally deferred packages immediately at startup.")
(defun vlaci-load-packages-incrementally (packages &optional now)
"Registers PACKAGES to be loaded incrementally.
If NOW is non-nil, load PACKAGES incrementally, in `doom-incremental-idle-timer'
intervals."
(if (not now)
(setq vlaci-incremental-packages (append vlaci-incremental-packages packages))
(while packages
(let ((req (pop packages)))
(unless (featurep req)
(message "Incrementally loading %s" req)
(condition-case e
(or (while-no-input
;; If `default-directory' is a directory that doesn't exist
;; or is unreadable, Emacs throws up file-missing errors, so
;; we set it to a directory we know exists and is readable.
(let ((default-directory user-emacs-directory)
(gc-cons-threshold most-positive-fixnum)
file-name-handler-alist)
(require req nil t))
t)
(push req packages))
((error debug)
(message "Failed to load '%s' package incrementally, because: %s"
req e)))
(if (not packages)
(message "Finished incremental loading")
(run-with-idle-timer vlaci-incremental-idle-timer
nil #'vlaci-load-packages-incrementally
packages t)
(setq packages nil)))))))
;;;###autoload
(defun vlaci-load-packages-incrementally-h ()
"Begin incrementally loading packages in `vlaci-incremental-packages'.
If this is a daemon session, load them all immediately instead."
(if vlaci-incremental-load-immediately
(mapc #'require (cdr vlaci-incremental-packages))
(when (numberp vlaci-incremental-first-idle-timer)
(run-with-idle-timer vlaci-incremental-first-idle-timer
nil #'vlaci-load-packages-incrementally
(cdr vlaci-incremental-packages) t))))
(add-hook 'emacs-startup-hook #'vlaci-load-packages-incrementally-h)
(require 'setup)
(setup-define :package
(lambda (package))
:documentation "Fake installation of PACKAGE."
:repeatable t
:shorthand #'cadr)
(setup-define :defer-incrementally
(lambda (&rest targets)
(vlaci-load-packages-incrementally targets)
:documentation "Load TARGETS incrementally"))
(setup emacs
(defun vl/welcome ()
(with-current-buffer (get-buffer-create "*scratch*")
(insert (format ";;
;; ██╗ ██╗██╗ ███████╗███╗ ███╗ █████╗ ██████╗███████╗
;; ██║ ██║██║ ██╔════╝████╗ ████║██╔══██╗██╔════╝██╔════╝
;; ╚██╗ ██╔╝██║ █████╗ ██╔████╔██║███████║██║ ███████╗
;; ╚████╔╝ ██║ ██╔══╝ ██║╚██╔╝██║██╔══██║██║ ╚════██║
;; ╚██╔╝ ██████╗ ███████╗██║ ╚═╝ ██║██║ ██║╚██████╗███████║
;; ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚══════╝
;;
;; Loading time : %s
;; Features : %s
"
(emacs-init-time)
(length features))))
(message (emacs-init-time)))
(:with-function vl/welcome
(:hook-into after-init-hook)))
Better keyboard-quit
Based on Prot’s Basic and capable configuration article.
(setup vlaci-emacs
(:global [remap keyboard-quit] #'vlaci-keyboard-quit-dwim))
;;;###autoload
(defun vlaci-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) ;; Do I need this?
(delete-completion-window))
((> (minibuffer-depth) 0)
(abort-recursive-edit))
(t
(keyboard-quit))))
Icons, icons, icons
(setup (:package nerd-icons))
(setup (:package nerd-icons-completion)
(:with-function nerd-icons-completion-marginalia-setup
(:hook-into marginalia-mode-hook)))
(setup (:package nerd-icons-corfu)
(:with-feature corfu
(:when-loaded
(add-to-list 'corfu-margin-formatters #'nerd-icons-corfu-formatter))))
(setup (:package nerd-icons-dired)
(:hook-into dired-mode-hook))
nerd-icons
nerd-icons-completion
nerd-icons-corfu
nerd-icons-dired
Undo
undo-fu
undo-fu-session
vundo
(setup (:package undo-fu)
(:option undo-limit (* 80 1024 1024)
undo-strong-limit (* 120 1024 1024)
undo-outer-limit (* 360 1024 1024)))
(setup (:package undo-fu-session)
(:with-mode undo-fu-session-global-mode
(:hook-into on-first-buffer-hook)))
(setup (:package vundo)
(:option vundo-compact-display t)
(:bind [remap keyboard-quit] #'vundo-quit))
Editing
(setup emacs
(:option tab-width 4
truncate-lines t))
Evil
evil
evil-collection
devil
(setup (:package evil evil-collection devil)
(:hook-into after-init-hook)
(:option
;; Will be handled by evil-collections
evil-want-keybinding nil
;; Make `Y` behave like `D`
evil-want-Y-yank-to-eol t
;; Do not extend visual selection to whole lines for ex commands
evil-ex-visual-char-range t
;; `*` and `#` selects symbols instead of words
evil-symbol-word-search t
;; Only highlight in the current window
evil-ex-interactive-search-highlight 'selected-window
;; Use vim-emulated search implementation
evil-search-module 'evil-search
;; Do not spam with error messages
evil-kbd-macro-suppress-motion-error t
evil-undo-system 'undo-fu
evil-visual-state-cursor 'hollow
evil-visual-update-x-selection-p nil
evil-move-cursor-back nil
evil-move-beyond-eol t)
(:also-load evil-collection)
(:also-load devil)
(:when-loaded
;;; delay loading evil-collection modules until they are needed
(dolist (mode evil-collection-mode-list)
(dolist (req (or (cdr-safe mode) (list mode)))
(with-eval-after-load req
(message "Loading evil-collection for mode %s" req)
(evil-collection-init (list mode)))))
(evil-collection-init
'(help
(buff-menu "buff-menu")
calc
image
elisp-mode
replace
(indent "indent")
(process-menu simple)
shortdoc
tabulated-list
tab-bar))
(evil-global-set-key 'normal (kbd "SPC") #'devil)
(evil-global-set-key 'insert [remap evil-complete-next] #'complete-symbol)
(evil-global-set-key 'motion (kbd ",") nil)
(:with-feature devil
(:when-loaded
(devil-set-key (kbd "SPC"))))))
(setup-define :ebind
(lambda (key command)
`(evil-define-key ,(setup-get 'evil-state) ,(setup-get 'map) ,key ,command))
:documentation "Bind KEY to COMMAND for the given EVIL state"
:repeatable t
:indent 0)
(setup-define :with-state
(lambda (state &rest body)
(let (bodies)
(push (setup-bind body (evil-state state)) bodies)
(macroexp-progn (nreverse bodies))))
:documentation "Use STATE for binding keys"
:indent 1)
(setup-define :evil
(lambda (&rest body)
(require 'evil)
`(:with-state 'motion ,@body))
:documentation "Bind KEYs to COMMANDs for the given EVIL state"
:ensure '(nil &rest kbd func)
:indent 0)
Navigation
evil-ts-obj = { url = "github:vlaci/evil-ts-obj"; flake = false; };
treesit-jump = { url = "github:vlaci/treesit-jump"; flake = false; };
ace-window
avy
swiper
evil-snipe
(mkPackage {
pname = "evil-ts-obj";
src = inputs.evil-ts-obj;
files = [ "lisp/*.el" ];
packageRequires = [ avy evil ];
})
(mkPackage {
pname = "treesit-jump";
src = inputs.treesit-jump;
files = [ "treesit-jump.el" "treesit-queries" ];
packageRequires = [ avy ];
})
(setup (:package avy)
(:option avy-keys '(?a ?r ?s ?t ?d ?h ?n ?e ?i ?o ?w ?f ?p ?l ?u ?y))
(:evil
(defvar avy-all-windows)
(defvar swiper-goto-start-of-match)
(evil-define-motion vlaci/goto-char-timer-or-swiper-isearch (_count)
:type inclusive
:jump t
:repat abort
(evil-without-repeat
(evil-enclose-avy-for-motion
(when (eq (avy-goto-char-timer) t)
(let ((swiper-goto-start-of-match (not evil-this-operator)))
(swiper-isearch avy-text))))))
(advice-add 'avy-resume :after #'evil-normal-state)
(:with-map 'global
(:with-state 'normal
(:ebind
"r" nil))
(:ebind
"r/" #'vlaci/goto-char-timer-or-swiper-isearch))))
(setup (:package evil-ts-obj)
(:hook-into
bash-ts-mode-hook
c-ts-mode-hook
c++-ts-mode-hook
nix-ts-mode-hook
python-ts-mode-hook
rust-ts-mode-hook
yaml-ts-mode-hook)
(:when-loaded
;; Free-up M-s prefix
(evil-define-key 'normal 'evil-ts-obj-mode
(kbd "M-s") nil
(kbd "M-S") nil
(kbd "M-i") #'evil-ts-obj-inject-down-dwim
(kbd "M-I") #'evil-ts-obj-inject-up-dwim)))
(setup (:package treesit-jump)
(:evil
(:with-map 'global
(:ebind "zj" #'treesit-jump-jump))))
(setup (:package evil-snipe)
(:hook-into on-first-input-hook)
(:with-mode evil-snipe-override-mode
(:hook-into on-first-input-hook))
(:option evil-snipe-override-evil-repeat-keys nil
evil-snipe-scope 'visible
evil-snipe-repeat-scope 'whole-visible
evil-snipe-smart-case t
evil-snipe-tab-increment t))
Completion
vertico
vertico-posframe
orderless
marginalia
consult
corfu
cape
embark
embark-consult
wgrep
From its README.
(setup (:package vertico vertico-posframe)
(:with-mode (vertico-mode vertico-multiform-mode)
(:hook-into on-first-input-hook))
(:with-map minibuffer-local-map
(:bind [escape] #'keyboard-quit))
(:option vertico-scroll-margin 0
vertico-count 17
vertico-resize t
vertico-cycle t
vertico-multiform-categories '((t
posframe
(vertico-posframe-poshandler . posframe-poshandler-frame-top-center)
(vertico-posframe-fallback-mode . vertico-buffer-mode)))
vertico-posframe-width 100))
;; A few more useful configurations...
(setup emacs
(:option
;; Support opening new minibuffers from inside existing minibuffers.
enable-recursive-minibuffers t
;; Hide commands in M-x which do not work in the current mode. Vertico
;; commands are hidden in normal buffers. This setting is useful beyond
;; Vertico.
read-extended-command-predicate #'command-completion-default-include-p)
;; Add prompt indicator to `completing-read-multiple'.
;; We display [CRM<separator>], e.g., [CRM,] if the separator is a comma.
(defun crm-indicator (args)
(cons (format "[CRM%s] %s"
(replace-regexp-in-string
"\\`\\[.*?]\\*\\|\\[.*?]\\*\\'" ""
crm-separator)
(car args))
(cdr args)))
(advice-add #'completing-read-multiple :filter-args #'crm-indicator)
;; Do not allow the cursor in the minibuffer prompt
(setq minibuffer-prompt-properties
'(read-only t cursor-intangible t face minibuffer-prompt))
(add-hook 'minibuffer-setup-hook #'cursor-intangible-mode))
Based on Minad’s configuration:
(setup (:package orderless)
(defun vl/orderless--consult-suffix ()
"Regexp which matches the end of string with Consult tofu support."
(if (and (boundp 'consult--tofu-char) (boundp 'consult--tofu-range))
(format "[%c-%c]*$"
consult--tofu-char
(+ consult--tofu-char consult--tofu-range -1))
"$"))
;; Recognizes the following patterns:
;; * .ext (file extension)
;; * regexp$ (regexp matching at end)
(defun vl/orderless-consult-dispatch (word _index _total)
(cond
;; Ensure that $ works with Consult commands, which add disambiguation suffixes
((string-suffix-p "$" word)
`(orderless-regexp . ,(concat (substring word 0 -1) (vl/orderless--consult-suffix))))
;; File extensions
((and (or minibuffer-completing-file-name
(derived-mode-p 'eshell-mode))
(string-match-p "\\`\\.." word))
`(orderless-regexp . ,(concat "\\." (substring word 1) (vl/orderless--consult-suffix))))))
(:once 'on-first-input-hook
(:require orderless)
;; Define orderless style with initialism by default
(orderless-define-completion-style vl/orderless-with-initialism
(orderless-matching-styles '(orderless-initialism orderless-literal orderless-regexp))))
;; Certain dynamic completion tables (completion-table-dynamic) do not work
;; properly with orderless. One can add basic as a fallback. Basic will only
;; be used when orderless fails, which happens only for these special
;; tables. Also note that you may want to configure special styles for special
;; completion categories, e.g., partial-completion for files.
(:option completion-styles '(orderless basic)
completion-category-defaults nil
;;; Enable partial-completion for files.
;;; Either give orderless precedence or partial-completion.
;;; Note that completion-category-overrides is not really an override,
;;; but rather prepended to the default completion-styles.
;; completion-category-overrides '((file (styles orderless partial-completion))) ;; orderless is tried first
completion-category-overrides '((file (styles partial-completion)) ;; partial-completion is tried first
;; enable initialism by default for symbols
(command (styles vl/orderless-with-initialism))
(variable (styles vl/orderless-with-initialism))
(symbol (styles vl/orderless-with-initialism)))
orderless-component-separator #'orderless-escapable-split-on-space ;; allow escaping space with backslash!
orderless-style-dispatchers (list #'vl/orderless-consult-dispatch
#'orderless-affix-dispatch)))
(setup (:package marginalia)
(:hook-into after-init-hook)
(:with-map minibuffer-local-map
(:bind "M-A" marginalia-cycle)))
(setup (:package consult)
(:global ;; C-c bindings in `mode-specific-map'
"C-c M-x" consult-mode-command
"C-c h" consult-history
"C-c k" consult-kmacro
"C-c m" consult-man
"C-c i" consult-info
[remap Info-search] #'consult-info
;; C-x bindings in `ctl-x-map'
"C-x M-:" consult-complex-command ;; orig. repeat-complex-command
"C-x b" consult-buffer ;; orig. switch-to-buffer
"C-x 4 b" consult-buffer-other-window ;; orig. switch-to-buffer-other-window
"C-x 5 b" consult-buffer-other-frame ;; orig. switch-to-buffer-other-frame
"C-x t b" consult-buffer-other-tab ;; orig. switch-to-buffer-other-tab
"C-x r b" consult-bookmark ;; orig. bookmark-jump
"C-x p b" consult-project-buffer ;; orig. project-switch-to-buffer
;; Custom M-# bindings for fast register access
"M-#" consult-register-load
"M-'" consult-register-store ;; orig. abbrev-prefix-mark (unrelated
"C-M-#" consult-register
;; Other custom bindings
"M-y" consult-yank-pop ;; orig. yank-pop
;; M-g bindings in `goto-map'
"M-g e" consult-compile-error
"M-g f" consult-flymake ;; Alternative: consult-flycheck
"M-g g" consult-goto-line ;; orig. goto-line
"M-g M-g" consult-goto-line ;; orig. goto-line
"M-g o" consult-outline ;; Alternative: consult-org-heading
"M-g m" consult-mark
"M-g k" consult-global-mark
"M-g i" consult-imenu
"M-g I" consult-imenu-multi
;; M-s bindings in `search-map'
"M-s d" consult-fd ;; Alternative: consult-find
"M-s c" consult-locate
"M-s g" consult-grep
"M-s G" consult-git-grep
"M-s r" consult-ripgrep
"M-s l" consult-line
"M-s L" consult-line-multi
"M-s k" consult-keep-lines
"M-s u" consult-focus-lines
;; Isearch integration
"M-s e" consult-isearch-history)
(:with-map isearch-mode-map
(:bind
"M-e" consult-isearch-history ;; orig. isearch-edit-string
"M-s e" consult-isearch-history ;; orig. isearch-edit-string
"M-s l" consult-line ;; needed by consult-line to detect isearch
"M-s L" consult-line-multi)) ;; needed by consult-line to detect isearch
;; Minibuffer history
(:with-map minibuffer-local-map
(:bind
"M-s" consult-history ;; orig. next-matching-history-element
"M-r" consult-history)) ;; orig. previous-matching-history-element
;; Enable automatic preview at point in the *Completions* buffer. This is
;; relevant when you use the default completion UI.
(:with-mode consult-preview-at-point-mode
(:hook-into completion-list-mode))
;; Tweak the register preview for `consult-register-load',
;; `consult-register-store' and the built-in commands. This improves the
;; register formatting, adds thin separator lines, register sorting and hides
;; the window mode line.
(advice-add #'register-preview :override #'consult-register-window)
(setq register-preview-delay 0.5)
;; Use Consult to select xref locations with preview
(setq xref-show-xrefs-function #'consult-xref
xref-show-definitions-function #'consult-xref)
(:when-loaded
;; Optionally configure preview. The default value
;; is 'any, such that any key triggers the preview.
;; (setq consult-preview-key 'any)
;; (setq consult-preview-key "M-.")
;; (setq consult-preview-key '("S-<down>" "S-<up>"))
;; For some commands and buffer sources it is useful to configure the
;; :preview-key on a per-command basis using the `consult-customize' macro.
(consult-customize
consult-theme :preview-key '(:debounce 0.2 any)
consult-ripgrep consult-git-grep consult-grep
consult-bookmark consult-recent-file consult-xref
consult--source-bookmark consult--source-file-register
consult--source-recent-file consult--source-project-recent-file
;; :preview-key "M-."
:preview-key '(:debounce 0.4 any))
;; Optionally configure the narrowing key.
;; Both < and C-+ work reasonably well.
(setq consult-narrow-key "<") ;; "C-+"
;; Optionally make narrowing help available in the minibuffer.
;; You may want to use `embark-prefix-help-command' or which-key instead.
;; (keymap-set consult-narrow-map (concat consult-narrow-key " ?") #'consult-narrow-help)
))
(setup (:package corfu)
(:with-mode global-corfu-mode
(:hook-into on-first-input-hook))
(:with-mode corfu-popupinfo-mode
(:hook-into on-first-input-hook)))
(setup emacs
;; TAB cycle if there are only few candidates
;; (completion-cycle-threshold 3)
;; Enable indentation+completion using the TAB key.
;; `completion-at-point' is often bound to M-TAB.
(:option tab-always-indent 'complete
;; Emacs 30 and newer: Disable Ispell completion function.
;; Try `cape-dict' as an alternative.
text-mode-ispell-word-completion nil
;; Hide commands in M-x which do not apply to the current mode. Corfu
;; commands are hidden, since they are not used via M-x. This setting is
;; useful beyond Corfu.
read-extended-command-predicate #'command-completion-default-include-p))
;; Use Dabbrev with Corfu!
(setup dabbrev
;; Swap M-/ and C-M-/
(:global "M-/" dabbrev-completion
"C-M-/" dabbrev-expand)
(:when-loaded
(add-to-list 'dabbrev-ignored-buffer-regexps "\\` ")
;; Since 29.1, use `dabbrev-ignored-buffer-regexps' on older.
(add-to-list 'dabbrev-ignored-buffer-modes 'doc-view-mode)
(add-to-list 'dabbrev-ignored-buffer-modes 'pdf-view-mode)
(add-to-list 'dabbrev-ignored-buffer-modes 'tags-table-mode)))
(setup (:package embark)
(:option embark-indicators
'(embark-minimal-indicator ; default is embark-mixed-indicator
embark-highlight-indicator
embark-isearch-highlight-indicator))
(:with-feature vertico
(:when-loaded
(add-to-list 'vertico-multiform-categories '(embark-keybinding grid))))
(setq prefix-help-command #'embark-prefix-help-command)
(:global [remap describe-bindings] #'embark-bindings
"C-;" #'embark-act
"M-." #'embark-dwim)
(:with-map minibuffer-local-map
(:bind "C-;" #'embark-act)))
Tree-Sitter
(treesit-grammars.with-grammars (
grammars:
with pkgs.lib;
pipe grammars [
(filterAttrs (name: _: name != "recurseForDerivations"))
builtins.attrValues
]
))
treesit-auto
(setup-define :autoload
(lambda (func)
(let ((fn (if (memq (car-safe func) '(quote function))
(cadr func)
func)))
`(unless (fboundp (quote ,fn))
(autoload (function ,fn) ,(symbol-name (setup-get 'feature)) nil t))))
:documentation "Autoload COMMAND if not already bound."
:repeatable t
:signature '(FUNC ...))
(setup (:package treesit-auto)
(:autoload 'global-treesit-auto-mode)
(:with-mode global-treesit-auto-mode
(:hook-into after-init-hook))
(:when-loaded
(delete 'dockerfile treesit-auto-langs)
(treesit-auto-add-to-auto-mode-alist 'all)))
LSP
emacs-lsp-booster = { url = "github:blahgeek/emacs-lsp-booster"; flake = false; };
eglot-booster = { url = "github:jdtsmith/eglot-booster"; flake = false; };
eglot-x = { url = "github:nemethf/eglot-x"; flake = false; };
sideline-eglot = { url = "github:emacs-sideline/sideline-eglot"; flake = false; };
final: prev:
{
eglot-booster = final.mkPackage {
pname = "eglot-booster";
src = inputs.eglot-booster;
};
eglot-x = final.mkPackage {
pname = "eglot-x";
src = inputs.eglot-x;
};
sideline-eglot = final.mkPackage {
pname = "sideline-eglot";
src = inputs.sideline-eglot;
packageRequires = [ final.sideline ];
};
emacs-lsp-booster = pkgs.rustPlatform.buildRustPackage rec {
pname = "emacs-lsp-booster";
version = "0.2.1";
src = inputs.emacs-lsp-booster;
cargoLock = {
lockFile = "${src}/Cargo.lock";
};
doCheck = false;
};
}
eglot-booster
eglot-x
emacs-lsp-booster
dape
sideline-flymake
sideline-eglot
basedpyright
(setup (:package eglot-booster)
(:option eglot-booster-io-only t))
(setup eglot
(:also-load eglot-booster)
(:also-load eglot-x)
(:when-loaded
(setq completion-category-defaults nil))
(advice-add 'eglot-completion-at-point :around #'cape-wrap-buster)
(defun vlaci/eglot-capf ()
(setq-local completion-at-point-functions
(list (cape-capf-super
#'eglot-completion-at-point
#'cape-dabbrev)))
(add-hook 'completion-at-point-functions #'cape-file))
(:with-function vlaci/eglot-capf
(:hook-into eglot-managed-mode-hook))
(:evil
(:ebind
",r" #'eglot-rename
",a" #'eglot-code-actions)
(:with-state 'insert
(:ebind
(kbd "C-.") #'eglot-code-actions))))
(setup (:package eglot-x)
(:when-loaded
(eglot-x-setup)))
(setup-define :lsp
(lambda ()
`(:hook eglot-ensure))
:documentation "Configure LSP")
(setup (:package dape)
(:option
dape-repl-use-shorthand t))
(setup (:package sideline sideline-flymake sideline-eglot)
(:option sideline-backends-right '(sideline-flymake sideline-eglot))
(:hook-into prog-mode-hook))
Dired
(setup dired
(:option dired-listing-switches "-Alh --group-directories-first --time-style=iso"
dired-kill-when-opening-new-dired-buffer t)
(:global "M-i" vl/window-dired-vc-root-left)
(:bind "C-<return>" vl/window-dired-open-directory)
(defun vl/window-dired-vc-root-left (&optional directory-path)
"Creates *Dired-Side* like an IDE side explorer"
(interactive)
(add-hook 'dired-mode-hook 'dired-hide-details-mode)
(let ((dir (if directory-path
(dired-noselect directory-path)
(if (eq (vc-root-dir) nil)
(dired-noselect default-directory)
(dired-noselect (vc-root-dir))))))
(display-buffer-in-side-window
dir `((side . left)
(slot . 0)
(window-width . 30)
(window-parameters . ((no-other-window . t)
(no-delete-other-windows . t)
(mode-line-format . (" "
"%b"))))))
(with-current-buffer dir
(let ((window (get-buffer-window dir)))
(when window
(select-window window)
(rename-buffer "*Dired-Side*"))))))
(defun vl/window-dired-open-directory ()
"Open the current directory in *Dired-Side* side window."
(interactive)
(vl/window-dired-vc-root-left (dired-get-file-for-visit))))
dirvish
pdf-tools
vips
ffmpegthumbnailer
mediainfo
epub-thumbnailer
p7zip
(setup (:package dirvish)
(:when-loaded (dirvish-override-dired-mode)))
Languages
eldev
nix-ts-mode
markdown-mode
just-ts-mode
polymode
rust-mode
dockerfile-mode
nil
llvmPackages.clang-tools
rust-analyzer
(setup (:package polymode))
(add-to-list 'auto-mode-alist '("\\.nix" . ordenada-nix-polymode))
(setup (:package nix-ts-mode)
(define-hostmode poly-nix-hostmode
:mode 'nix-mode)
(define-auto-innermode poly-nix-dynamic-innermode
:head-matcher (rx "#" blank (+ (any "a-z" "-")) (+ (any "\n" blank)) "''\n")
:tail-matcher (rx bol (+ blank) "'';")
:mode-matcher (cons (rx "#" blank (group (+ (any "a-z" "-"))) (* anychar)) 1)
:head-mode 'host
:tail-mode 'host)
(define-innermode poly-nix-interpolation-innermode
:mode 'nix-mode
:head-matcher (rx "${")
:tail-matcher #'pm-forward-sexp-tail-matcher
:head-mode 'body
:tail-mode 'body
:can-nest t)
(define-polymode poly-nix-mode
:hostmode 'poly-nix-hostmode
:innermodes '(poly-nix-dynamic-innermode))
(:with-mode poly-nix-mode
(:file-match "\\.nix\\'"))
(defalias 'nix-mode 'nix-ts-mode) ;; For org-mode code blocks to work
(:lsp))
(setup (:package rust-ts-mode)
(:lsp))
(setup python-ts-mode
(:lsp))
(setup c-or-c++-ts-mode
(:lsp))
(setup (:package just-ts-mode)
(define-hostmode poly-just-hostmode
:mode 'just-ts-mode)
(defun vlaci/poly-get-innermode-for-exe (re)
(re-search-forward re (point-at-eol) t)
(let ((exe (match-string-no-properties 1)))
(cond ((equal exe "emacs") "emacs-lisp")
(t exe))))
(define-auto-innermode poly-just-innermode
:head-matcher (rx bol (+ (any blank)) "#!" (+ (any "a-z0-9_/ -")) "\n")
:tail-matcher #'pm-same-indent-tail-matcher
:mode-matcher (apply-partially #'vlaci/poly-get-innermode-for-exe (rx (+? anychar) "bin/env " (? "-S ") (group (+ (any "a-z-"))) (* anychar)))
:head-mode 'host
:tail-mode 'host)
(define-auto-innermode poly-just-script-innermode
:head-matcher (rx bol "[script('" (+? anychar) ":" (* (not "\n")) "\n")
:tail-matcher #'pm-same-indent-tail-matcher
:mode-matcher (apply-partially #'vlaci/poly-get-innermode-for-exe (rx bol "[script('" (group (+ (not "'"))) (* anychar)))
:head-mode 'host
:tail-mode 'host)
(define-polymode poly-just-mode
:hostmode 'poly-just-hostmode
:innermodes '(poly-just-innermode poly-just-script-innermode))
(:with-mode poly-just-mode
(:file-match (rx (or "justfile" ".just") string-end))))
(setup (:package markdown-mode))
Direnv
envrc
Customizations are lifted from doomemacs
(setup (:package envrc)
(:with-mode envrc-global-mode
(:hook-into on-first-file-hook))
(defun vl/direnv-init-global-mode-earlier-h ()
(let ((fn #'envrc-global-mode-enable-in-buffer))
(if (not envrc-global-mode)
(remove-hook 'change-major-mode-after-body-hook fn)
(remove-hook 'after-change-major-mode-hook fn)
(add-hook 'change-major-mode-after-body-hook fn 100))))
(add-hook 'envrc-global-mode-hook #'vl/direnv-init-global-mode-earlier-h)
(defvar vl/orig-exec-path exec-path)
(define-advice envrc--update (:around (fn &rest args) vl/envrc--debounce-add-extra-path-a)
"Update only on non internal envrc related buffers keeping original path entries as well"
(when (not (string-prefix-p "*envrc" (buffer-name)))
(apply fn args)
(setq-local exec-path (append exec-path vl/orig-exec-path)))))
Spell-checking
jinx
(setup (:package jinx)
(:with-mode global-jinx-mode
(:hook-into on-first-buffer-hook))
(:option jinx-languages "en_US hu_HU")
(:evil
(:ebind
[remap evil-next-flyspell-error] #'jinx-next
[remap evil-prev-flyspell-error] #'jinx-previous
[remap ispell-word] #'jinx-correct))
(:when-loaded
(add-to-list 'vertico-multiform-categories
'(jinx grid (vertico-grid-annotate . 20)))))
Magit
magit
(setup (:package magit)
(:option magit-prefer-remote-upstream t
magit-save-repository-buffers nil
magit-diff-refine-hunk t
magit-define-global-key-bindings 'recommended
git-commit-major-mode 'markdown-mode)
(:when-loaded
(transient-append-suffix 'magit-pull "-r"
'("-a" "Autostash" "--autostash"))
(add-to-list 'font-lock-ignore '(git-commit-mode markdown-fontify-headings)))
(:with-feature magit-commit
(:when-loaded
(transient-replace-suffix 'magit-commit 'magit-commit-autofixup
'("x" "Absorb changes" magit-commit-absorb))
(setq transient-levels '((magit-commit (magit-commit-absorb . 1))))))
(:with-feature project
(:when-loaded
(define-key project-prefix-map "m" #'magit-project-status)
(add-to-list 'project-switch-commands '(magit-project-status "Magit") t)))
(:with-feature smerge-mode
(:when-loaded
(map-keymap
(lambda (_key cmd)
(when (symbolp cmd)
(put cmd 'repeat-map 'smerge-basic-map)))
smerge-basic-map))))
(setup ediff
(:option ediff-keep-variants nil
ediff-split-window-function #'split-window-horizontally
ediff-window-setup-function #'ediff-setup-windows-plain))
Formatting
(setup emacs
(:option indent-tabs-mode nil
mouse-yank-at-point t)) ;; paste at keyboard cursor instead of mouse pointer location
apheleia
nixfmt-rfc-style
nodePackages.prettier
(setup (:package apheleia)
(:with-mode apheleia-global-mode
(:hook-into on-first-file-hook))
(:when-loaded
;; do not use apheleia-npx wrapper
(dolist (key (list
'prettier
'prettier-css
'prettier-html
'prettier-graphql
'prettier-javascript
'prettier-json
'prettier-markdown
'prettier-ruby
'prettier-scss
'prettier-scsss
'prettier-svelte
'pretter-typescript
'prettier-yaml))
(setf (alist-get key apheleia-formatters) (cdr (alist-get key apheleia-formatters))))
(setf (alist-get 'ruff-check apheleia-formatters) (list "ruff" "check" "--fix" "--exit-zero" "-"))
(setf (alist-get 'ruff-format apheleia-formatters) (list "ruff" "format" "-"))
(setf (alist-get 'python-mode apheleia-mode-alist) '(ruff-check ruff-format))
(setf (alist-get 'python-ts-mode apheleia-mode-alist) '(ruff-check ruff-format))))
Passwords
auth-source-1password
(setup (:package auth-source-1password)
(:with-function auth-source-1password-enable
(:hook-into on-first-buffer-hook))
(:option auth-source-1password-vault "Emacs"))
AI
gptel
chatgpt-shell
(setup (:package chatgpt-shell)
(:option
chatgpt-shell-perplexity-api-key
(lambda() (auth-source-pick-first-password :host "Perplexity" :user "credential"))))
(setup (:package gptel)
(:when-loaded
(:option gptel-model 'sonar
gptel-backend (gptel-make-perplexity "Perplexity"
:key (lambda() (auth-source-pick-first-password :host "Perplexity" :user "credential"))
:stream t))))
Projects
(setup project
(define-advice project-current (:around (fun &rest args) vl/project-current-per-frame-a)
(let ((proj (frame-parameter nil 'vl/project-current)))
(unless proj
(setq proj (apply fun args))
(modify-frame-parameters nil `((vl/project-current . ,proj))))
proj))
(define-advice project-switch-project (:before (&rest _) vl/project-switch-project-per-frame-a)
(modify-frame-parameters nil '((vl/project-current . nil)))))
Org
org-present
visual-fill-column
(setup (:package org-present))
(setup (:package visual-fill-column))