/dotemacs

Emacs config

Primary LanguageEmacs Lisp

Emacs Configuration

Overview

This configuration is mostly based on:

Early initialisation

The early-init.el file is supported from version 27.1. It allows to set variables (that have effect on startup speed) early in the initialisation to make Emacs start faster.

The content of early-init.el file:

(defvar gc-cons-percentage-default gc-cons-percentage
  "The default value for `gc-cons-percentage'.")

(defvar file-name-handler-alist-default file-name-handler-alist
  "The default value for `file-name-handler-alist'.")

;; Defer garbage collection further back in the startup process.
(setq gc-cons-threshold most-positive-fixnum)
(setq gc-cons-percentage 0.5)

;; For every .el and .elc file loaded during start up, Emacs runs those
;; regexps against it.
(setq file-name-handler-alist nil)

;; Do not load site-wide runtime initializations.
(setq site-run-file nil)

;; Do not initialise the package manager.  This is done later.
(setq package-enable-at-startup nil)

;; Do not resize the frame at this early stage.
(setq frame-inhibit-implied-resize t)

;; Faster to disable GUI elements here (before they've been
;; initialized).
(menu-bar-mode -1)
(tool-bar-mode -1)
(scroll-bar-mode -1)
(tooltip-mode -1)
(setq use-file-dialog nil)
(setq use-dialog-box nil)

(setq inhibit-startup-screen t)

(provide 'early-init)

;;; early-init.el ends here

Initialisation

The main configuration file init.el starts here.

Emacs version check

The minimal supported version of Emacs is 28.0.

(when (version< emacs-version "28.0")
  (error "Emacs 28.0 or newer versions are required!"))

Garbage collector tweaks

In order to call garbage collection less frequently the gc-cons-threshold is increased from its default value of 800 KB. Check this link to find out more details. Similarly, the value of gc-cons-threshold-upper-limit is increased.

(defvar gc-cons-threshold-default
  (if (display-graphic-p) 16000000 1600000)
  "The default value for `gc-cons-threshold'.")

(defvar gc-cons-threshold-upper-limit
  (if (display-graphic-p) 128000000 32000000)
  "The upper limit value for `gc-cons-threshold' to defer it.")

(defun j-setup-default-startup-values ()
  "Setup default startup values."
  (setq gc-cons-threshold gc-cons-threshold-default)
  (setq gc-cons-percentage gc-cons-percentage-default)
  (setq file-name-handler-alist file-name-handler-alist-default)
  ;; Unset the symbols created in early-init.el.
  (makunbound 'gc-cons-percentage-default)
  (makunbound 'file-name-handler-alist-default))

(defun j-minibuffer-setup-hook ()
  "Setup `gc-cons-threshold' to a large number.

When minibuffer is open, garbage collection never occurs so there
is no freezing."
  (setq gc-cons-threshold gc-cons-threshold-upper-limit))

(defun j-minibuffer-exit-hook ()
  "Setup `gc-cons-threshold' to a small number.

When a selection is made or minibuffer is cancelled, garbage
collection kicks off."
  (setq gc-cons-threshold gc-cons-threshold-default))

(defun j-garbage-collect-when-minibuffer-exit ()
  "Setup setup and exit hooks for minibuffer."
  (add-hook 'minibuffer-setup-hook #'j-minibuffer-setup-hook)
  (add-hook 'minibuffer-exit-hook #'j-minibuffer-exit-hook))

(defun j-garbage-collect-when-unfocused ()
  "Setup garbage collection when unfocused."
  (if (boundp 'after-focus-change-function)
      (add-function :after after-focus-change-function
                    (lambda ()
                      (unless (frame-focus-state)
                        (garbage-collect))))
    (add-hook 'after-focus-change-function #'garbage-collect)))

(add-hook 'emacs-startup-hook #'j-setup-default-startup-values)
(add-hook 'emacs-startup-hook #'j-garbage-collect-when-minibuffer-exit)
(add-hook 'emacs-startup-hook #'j-garbage-collect-when-unfocused)

Package management

Package manager

The default package manager for Emacs is package.el. It downloads the packages as tarballs. The straight.el replaces the default package manager. The main difference is that the straight.el downloads the packages as git repositories, not as tarballs. It is well integrated with use-package.

(setq straight-use-package-by-default nil)

(defvar bootstrap-version)
(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))

Load and configure use-package

use-package is not a package manager (it doesn’t list, install or remove packages). Instead, it uses declarative syntax to configure packages.

(straight-use-package 'use-package)

(eval-and-compile
  ;; Needed for straight.el.
  (setq use-package-always-ensure nil)
  (setq use-package-always-defer nil)
  (setq use-package-always-demand nil)
  (setq use-package-expand-minimally nil)
  (setq use-package-enable-imenu-support t)
  ;;(setq use-package-hook-name-suffix nil)
  (setq use-package-compute-statistics nil))

Emacs related files

Configuration and transient files

To keep the home directory clean, the configuration files are placed in ~/.config/emacs directory. The configuration files include:

  • early-init.el;
  • init.el;
  • j-lisp directory with custom libraries (must be added to the load-path);
  • straight directory with package repositories.

The other Emacs related files such as history, backup files, etc. are in ~/.cache/emacs directory. The package no-littering takes care of organising files in this directory.

(defconst j-emacs-config-directory user-emacs-directory
  "Directory with Emacs configuration files.")

(defconst j-emacs-config-early-init-el
  (concat j-emacs-config-directory "early-init-test.el")
  "The file with early initialisation configuration.")

(defconst j-emacs-config-init-el
  (concat j-emacs-config-directory "init-test.el")
  "The main Emacs configuration file.")

(defconst j-emacs-config-source-org
  (concat j-emacs-config-directory "README.org")
  "The org file with all the Emacs configuration.")

(defconst j-emacs-cache-directory (expand-file-name "~/.cache/emacs/")
  "Directory with Emacs transient files.")

(add-to-list 'load-path (concat j-emacs-config-directory "j-lisp"))

(setq user-emacs-directory j-emacs-cache-directory)

(use-package no-littering
  :straight t)

Customisation settings file

The customisation settings file is placed in ~/.cache/emacs/etc directory.

(setq custom-file (no-littering-expand-etc-file-name "custom.el"))
(load custom-file 'noerror)

Base settings

Server mode

The first running process of Emacs is started as server so Emacs clients can connect to it. Calling emacsclient (with or without --create-frame), will share the same buffer list and data as the original running process (server). The server persists for as long as there is an Emacs frame attached to it.

(use-package server
  :hook
  (after-init . server-mode))

Set UTF-8 as default encoding

UTF-8 is the default encoding. Check this article to find out how to setup the default and other encodings.

(set-charset-priority 'unicode)

(prefer-coding-system 'utf-8)
(set-language-environment 'utf-8)
(set-default-coding-systems 'utf-8)
(set-buffer-file-coding-system 'utf-8)
(set-clipboard-coding-system 'utf-8)
(set-file-name-coding-system 'utf-8)
(set-keyboard-coding-system 'utf-8)
(set-terminal-coding-system 'utf-8)
(set-selection-coding-system 'utf-8)
(modify-coding-system-alist 'process "*" 'utf-8)

Bidirectional writing and so-long.el

In order to improve the performance of Emacs, we can allow support only for languages that are read/written from left to right. This reduces number of line scans (for example, a check for Arabic languages is not done). Learn more in Comprehensive guide on handling long lines in Emacs.

(setq-default bidi-paragraph-direction 'left-to-right)
(setq bidi-inhibit-bpa t)

;; Disable slow minor modes when reading very long lines to speed up
;; Emacs.
(use-package so-long
  :config
  (global-so-long-mode +1))

Common functions

The j-lisp/j-common.el defines commonly used functions within this configuration.

;;; j-common.el --- Commonly used functions -*- lexical-binding: t -*-

;;; Commentary:
;;
;; Commonly used function.

;;; Code:

(defgroup j-common ()
  "Commonly used functions."
  :group 'editing)

;;;###autoload
(defconst j-common-linuxp
  (eq system-type 'gnu/linux)
  "Are we running on a GNU/Linux system?")

;;;###autoload
(defconst j-common-macp
  (eq system-type 'darwin)
  "Are we running on a Mac system?")

;;;###autoload
(defconst j-common-guip
  (display-graphic-p)
  "Are we using GUI?")

;;;###autoload
(defconst j-common-rootp
  (string-equal "root" (getenv "USER"))
  "Are you a ROOT user?")

;;;###autoload
(defun j-common-number-negative (n)
  "Make N negative."
  (if (and (numberp n) (> n 0))
      (* -1 n)
    (error "%s is not a valid positive number" n)))

(provide 'j-common)
;;; j-common.el ends here

Load j-common package.

(use-package j-common
  :straight (:type built-in)
  :demand t)

Common and helper commands

;;; j-simple.el --- Generic commonly used commands -*- lexical-binding: t -*-

;;; Commentary:
;;
;; Generic commonly used commands.

;;; Code:

(defgroup j-simple ()
  "Generic commonly used commands."
  :group 'editing)

;; Commands for lines.

;;;###autoload
(defun j-simple-new-line-below (&optional arg)
  "Create an empty line below the current one.
Move the point to the absolute beginning.  Adapt indentation by
passing optional prefix ARG (\\[universal-argument]).  Also see
`j-simple-new-line-above'."
  (interactive "P")
  (end-of-line)
  (if arg
      (newline-and-indent)
    (newline)))

;;;###autoload
(defun j-simple-new-line-above (&optional arg)
  "Create an empty line above the current one.
Move the point to the absolute beginning.  Adapt indentation by
passing optional prefix ARG (\\[universal-argument])."
  (interactive "P")
  (let ((indent (or arg nil)))
    (if (or (bobp)
            (eq (line-number-at-pos) 1))
        (progn
          (beginning-of-line)
          (newline)
          (forward-line -1))
      (forward-line -1)
      (j-simple-new-line-below indent))))

;;;###autoload
(defun j-simple-kill-line (&optional arg)
  "Kill to the end of the line, the whole line on the next call.
If ARG is specified, do not modify the behaviour of `kill-line'.
line, kill whole line."
  (interactive "P")
  (if arg
      (kill-line arg)
    (if (eq (point-at-eol) (point))
        (kill-line 0)
      (kill-line))))

;;;###autoload
(defun j-simple-kill-line-backward ()
  "Kill from point to the beginning of the line."
  (interactive)
  (kill-line 0))

;;;###autoload
(defun j-simple-yank-replace-line-or-region ()
  "Replace line or region with latest kill.
This command can then be followed by the standard
`yank-pop' (default is bound to \\[yank-pop])."
  (interactive)
  (if (use-region-p)
      (delete-region (region-beginning) (region-end))
    (delete-region (point-at-bol) (point-at-eol)))
  (yank))

;; Commands for text insertion or manipulation.

;; TODO

;; Commands for object transposition.

(defmacro j-simple-transpose (name scope &optional doc)
  "Macro to produce transposition functions.
NAME is the function's symbol.  SCOPE is the text object to
operate on.  Optional DOC is the function's docstring.

Transposition over an active region will swap the object at
mark (region beginning) with the one at point (region end)"
  `(defun ,name (arg)
     ,doc
     (interactive "p")
     (let ((x (format "%s-%s" "transpose" ,scope)))
       (if (use-region-p)
           (funcall (intern x) 0)
         (funcall (intern x) arg)))))

(j-simple-transpose
 j-simple-transpose-lines
 "lines"
 "Transpose lines or swap over active region.")

(j-simple-transpose
 j-simple-transpose-paragraphs
 "paragraphs"
 "Transpose paragraphs or swap over active region.")

(j-simple-transpose
 j-simple-transpose-sentences
 "sentences"
 "Transpose sentences or swap over active region.")

(j-simple-transpose
 j-simple-transpose-sexps
 "sexps"
 "Transpose balanced expressions or swap over active region.")

;;;###autoload
(defun j-simple-transpose-chars ()
  "Always transposes the two characters before point.
There is no 'dragging' the character forward.  This is the
behaviour of `transpose-chars' when point is at the end of the
line."
  (interactive)
  (transpose-chars -1)
  (forward-char))

;;;###autoload
(defun j-simple-transpose-words (arg)
  "Transpose ARG words.

If region is active, swap the word at mark (region beginning)
with the one at point (region end).

Otherwise, and while inside a sentence, this behaves as the
built-in `transpose-words', dragging forward the word behind the
point.  The difference lies in its behaviour at the end or
beginning of a line, where it will always transpose the word at
point with the one behind or ahead of it (effectively the
last/first two words)."
  (interactive "p")
  (cond
   ((use-region-p)
    (transpose-words 0))
   ((eq (point) (point-at-eol))
    (transpose-words -1))
   ((eq (point) (point-at-bol))
    (forward-word 1)
    (transpose-words 1))
   (t
    (transpose-words arg))))

;; Commands for marking syntactic constructs


;; Commands for building and loading Emacs config.

;;;###autoload
(defun j-simple-build-emacs-config ()
  "Generate Emacs configuration files from Org file."
  (interactive)
  (org-babel-tangle-file j-emacs-config-source-org))

(provide 'j-simple)
;;; j-simple.el ends here

Load j-simple package.

(use-package j-simple
  :straight (:type built-in)
  :demand t)

Global keybindings

Modifier keys: Super and Meta

Set super and meta keys for different operating systems.

(cond (j-common-macp
       (setq mac-option-modifier 'super)
       (setq mac-command-modifier 'meta))
      (t nil))

General key bindings

The most essential key bindings:

  • moving the point
    • by single char, word, sentence, paragraph
    Key bindingDescription
    C-fforward-char
    C-bbackward-char
    M-fforward-word
    M-bbackward-word
    C-M-fforward-sexp
    C-M-bbackward-sexp
    M-aforward-sentence
    M-ebackward-sentence
    M-}forward-paragraph
    M-{backward-paragraph

    Moving by s-expression (sexp for short) is useful in code. For example, a sexp in Lisp is a block of code enclosed in parentheses.

    In order to distinguish between an abbreviation and a sentence end, the sentence is considered to end with two spaces or a new line after ., ? or ! (the abbreviation ends with just single space). This is also American typist’s convention and it’s recommended practise in Emacs.

    (setq sentence-end-double-space t)
    (setq sentence-end-without-period nil)
    (setq colon-double-space nil)
    (setq use-hard-newlines nil)
        

    Paragraph is basically any section of text separated by blank lines. This can be used in code (see paragraph-start variable).

    • to next/previous line
    Key bindingDescription
    C-nnext-line
    C-pprevious-line
    • to beginning/end of line
    Key bindingDescription
    C-amove-beginning-of-line
    C-emove-end-of-line

    Note that there is mwin package that improves this behaviour (TODO: link).

    • to beginning/end of buffer
    Key bindingDescription
    M-<beginning-of-buffer
    M->end-of-buffer

    Note that there is beginend package that improves this behaviour (TODO: link).

    • to a specific line/column
    Key bindingDescription
    M-g M-ggoto-line
    M-g <tab>move-to-column

    Note that goto-line is replaced by and improved version - consult-goto-line with an additional key binding M-g g (TODO: link).

  • scrolling and centering the window
    Key bindingDescription
    C-vscroll-up-command
    M-vscroll-down-command
    C-lrecenter-top-bottom
    C-M-lreposition-window

    recenter-top-bottom makes the line with the point to be in the middle/top/bottom of the window depending on how many times it’s used in a row. reposition-window scrolls the screen so it fits the current “thing” at point as much as possible in the window (paragraph, function definition, etc.).

Disable unused global key bindings.

(let ((map global-map))
  ;; Disable `suspend-emacs'.
  (define-key map (kbd "C-x C-z") nil)
  ;; Disable `view-hello-file'.
  (define-key map (kbd "C-h h") nil)
  ;; Disable `tmm-menubar'.
  (define-key map (kbd "M-`") nil))

Redefine or enhance global key bindings.

(let ((map global-map))
  ;; Commands for help.
  (define-key map (kbd "C-h K") #'describe-keymap)
  ;; Commands for lines.
  (define-key map (kbd "<C-return>") #'j-simple-new-line-below)
  (define-key map (kbd "<C-S-return>") #'j-simple-new-line-above)
  (define-key map (kbd "C-k") #'j-simple-kill-line)
  (define-key map (kbd "M-k") #'j-simple-kill-line-backward)
  (define-key map (kbd "C-S-y") #'j-simple-yank-replace-line-or-region)
  ;; TODO: line joins
  ;; Commands for object transposition.
  (define-key map (kbd "C-t") #'j-simple-transpose-chars)
  (define-key map (kbd "C-x C-t") #'j-simple-transpose-lines)
  (define-key map (kbd "C-S-t") #'j-simple-transpose-paragraphs)
  (define-key map (kbd "C-x M-t") #'j-simple-transpose-sentences)
  (define-key map (kbd "C-M-t") #'j-simple-transpose-sexps)
  (define-key map (kbd "M-t") #'j-simple-transpose-words))

The whole-line-or-region package changes behaviour of killing, yanking and commenting of lines and regions:

  • if no region is activated, the current line is copied/yanked/commented;
  • if a region is activated, the whole region is copied/yanked/commented;
  • with numeric prefix, it possible to operate on multiple lines starting with the current line.
(use-package whole-line-or-region
  :straight t
  :config
  (whole-line-or-region-global-mode +1))

The expand-region package replaces:

  • mark-word which doesn’t mark the whole word if the cursor is in the middle, only marks the part of the word from the cursor to the end of the word. er/mark-word works better as it marks the whole word regardless of where the cursor is placed;
  • mark-sexp is replaced with er/expand-region which with every call marks more context (sexp).
(use-package expand-region
  :straight t
  :bind
  (;; C-@
   ([remap mark-word] . er/mark-word)
   ;; C-M-@ or C-M-SPC
   ([remap mark-sexp] . er/expand-region)))

The behaviour of C-a and C-e is changed to move the cursor to the first/last actionable character of the line.

(use-package mwim
  :straight t
  :bind
  (;; C-a
   ([remap move-beginning-of-line] . mwim-beginning-of-code-or-line)
   ;; C-e
   ([remap move-end-of-line] . mwim-end-of-code-or-line)))

The behaviour of M-< and M-> is changed to move to the first/last actionable point in a buffer (DWIM style).

(use-package beginend
  :straight t
  :config
  (beginend-global-mode +1))

The subword package changes the way how word boundaries are treated in programming modes. For example, “CamelCase” are two words as well as “foo_bar”.

(use-package subword
  :hook (prog-mode . subword-mode))

The hungry-delete packages deletes multiple white chars at once, until there is a non-white char.

(use-package hungry-delete
  :straight t
  :config
  (setq-default hungry-delete-chars-to-skip " \t\f\v")
  (global-hungry-delete-mode +1))

If there is some text selected and we start typing, the selected text is deleted and replaced with the newly typed text.

(use-package delsel
  :config
  (delete-selection-mode +1))

The helpful package is an alternative to the built-in Emacs help. It provides more contextual information. Note that helpful-callable includes both functions and macros.

(use-package helpful
  :straight t
  :bind
  (("s-h" . helpful-at-point)
   ("C-h f" . helpful-callable)
   ("C-h v" . helpful-variable)
   ("C-h k" . helpful-key)))

The goto-last-change package makes it possible to move the cursor back to the last change.

(use-package goto-last-change
  :straight t
  :bind
  ("C-z" . goto-last-change))

Key bindings help

The which-key is a minor mode that displays the key bindings following currently entered incomplete command (a prefix).

(use-package which-key
  :straight t
  :config
  (setq which-key-dont-use-unicode t)
  (setq which-key-add-column-padding 2)
  (setq which-key-show-early-on-C-h nil)
  (setq which-key-idle-delay 0.8)
  (setq which-key-idle-secondary-delay 0.05)
  (setq which-key-popup-type 'side-window)
  (setq which-key-show-prefix 'echo)
  (setq which-key-max-display-columns 3)
  (setq which-key-separator "  ")
  (setq which-key-special-keys nil)
  (setq which-key-paging-key "<down>")
  (which-key-mode +1))

Theme

The themes have documentation.

(use-package modus-themes
  :straight t
  :init
  (setq modus-themes-bold-constructs t)
  (setq modus-themes-slanted-constructs t)
  (setq modus-themes-syntax 'green-strings)
  (setq modus-themes-prompts 'subtle-accented)
  (setq modus-themes-mode-line nil)
  (setq modus-themes-fringes 'subtle)
  (setq modus-themes-lang-checkers 'subtle-foreground-straight-underline)
  (setq modus-themes-intense-hl-line t)
  (setq modus-themes-paren-match 'intense-bold)
  (setq modus-themes-org-blocks 'greyscale)
  (setq modus-themes-org-habit 'traffic-light)
  (setq modus-themes-scale-headings t)
  (modus-themes-load-themes)
  :config
  (modus-themes-load-vivendi)
  :bind
  ("C-c t" . modus-themes-toggle))

Font

(defconst j-font-sizes-families-alist
  '(("phone" . (110 "Hack" "Source Serif Variable"))
    ("laptop" . (120 "Hack" "Source Serif Variable"))
    ("desktop" . (130 "Hack" "Source Serif Variable"))
    ("presentation" . (180 "Iosevka Nerd Font Mono" "Source Serif Pro")))
  "Alist of desired typefaces and their point sizes.

Each association consists of a display type mapped to a point
size, followed by monospaced and proportionately spaced font
names.  The monospaced typeface is meant to be applied to the
`default' and `fixed-pitch' faces.  The proportionately spaced
font is intended for the `variable-pitch' face.")

(defun j-set-font-face-attributes (height fixed-font variable-font)
  "Set font face attributes.

HEIGHT is the font's point size, represented as either '10' or
'10.5'.  FIXED-FONT is a fixed pitch typeface (also the default
one).  VARIABLE-FONT is proportionally spaced type face."
  (set-face-attribute 'default nil :family fixed-font :height height)
  (set-face-attribute 'fixed-pitch nil :family fixed-font)
  (set-face-attribute 'variable-pitch nil :family variable-font))

(defun j-set-font-for-display (display)
  "Set defaults based on DISPLAY."
  (let* ((font-data (assoc display j-font-sizes-families-alist))
         (height (nth 1 font-data))
         (fixed-font (nth 2 font-data))
         (variable-font (nth 3 font-data)))
    (j-set-font-face-attributes height fixed-font variable-font)))

;; TODO: determine pixel width for phone.
(defun j-get-display ()
  "Get display size."
  (if (<= (display-pixel-width) 1280)
	  "laptop"
    "desktop"))

(defun j-set-font-init ()
  "Set font for the current display."
  (if j-common-guip
      (j-set-font-for-display (j-get-display))
    (user-error "Not running a graphical Emacs, cannot set font")))

(add-hook 'after-init-hook #'j-set-font-init)

(defun j-font-mono-p (font)
  "Check if FONT is monospaced."
  (when-let ((info (font-info font)))
    ;; If the string is found the match function returns an integer.
    (integerp (string-match-p "spacing=100" (aref info 1)))))

;; Set fixed-pitch and variable-pitch fonts and font height
;; interactively. Mainly for testing purposes to check different font families.
(defun j-set-font ()
  "Set font."
  (interactive)
  (when sys/guip
    (let* ((font-groups (seq-group-by #'j-font-mono-p (font-family-list)))
           fixed-fonts
           variable-fonts
           all-fonts
           fixed-font
           variable-font
           (heights (mapcar #'number-to-string (list 110 115 120 125 130 135 140)))
           height)
           (if (caar font-groups)
               (setq fixed-fonts (cdar font-groups)
                     variable-fonts (cdadr font-groups))
             (setq fixed-fonts (cdadr font-groups)
                   variable-fonts (cdar font-groups)))
           (setq all-fonts (append variable-fonts fixed-fonts))
           (setq fixed-font (completing-read "Select fixed pitch font: " fixed-fonts nil t))
           (setq variable-font (completing-read "Select variable pitch font: " all-fonts nil t))
           (setq height (completing-read "Select or insert font height: " heights nil))
           (j-set-font-face-attributes (string-to-number height) fixed-font variable-font))))

History and backups

This section contains configuration of packages that are used for making file backups, keeping history of cursor position, file changes, etc.

These packages produce files where the history and backups are kept. The location of these files is not configured in this section, the package no-littering has sane defaults which are not overwritten here.

Desktop

The built-in desktop package saves the state of the desktop when Emacs is closed or crashes. The desktop state is read on the next Emacs startup and restores:

  • buffers (desktop-restore-eager restores just a couple of buffers, the rest is restored lazily);
  • frame configuration including windows (with their position) and workspaces. The alternative to storing the frame configuration is using registers with C-x r f and reading the it back using C-x r j.
(use-package desktop
  :config
  (setq desktop-base-file-name "desktop")
  (setq desktop-base-lock-name "desktop.lock")
  (setq desktop-auto-save-timeout 60)
  (setq desktop-restore-eager 5)
  (setq desktop-restore-frames t)
  (setq desktop-files-not-to-save nil)
  (setq desktop-globals-to-clear nil)
  (setq desktop-load-locked-desktop t)
  (setq desktop-missing-file-warning t)
  (setq desktop-save 'ask-if-new)
  (desktop-save-mode +1))

Minibuffer history

Remember actions related to the minibuffer, such as input and choices. The history is read by the completion frameworks.

(use-package savehist
  :config
  (setq history-length 10000)
  (setq history-delete-duplicates t)
  (setq savehist-autosave-interval 60)
  (setq savehist-additional-variables '(search-ring regexp-search-ring))
  (setq savehist-save-minibuffer-history t)
  (savehist-mode +1))

Cursor position history

Remember where the cursor position is in any file.

(use-package saveplace
  :config
  (setq save-place-forget-unreadable-files t)
  (save-place-mode +1))

File backup

Keep backups of visited files. The explanation of some of the settings:

  • make-backup-files - make a backup of a file when it’s saved the first time;
  • vc-make-backup-files - backup also versioned files (git, svn, etc.);
  • backup-by-copying - don’t clobber symlinks;
  • version-control - version numbers for backup files;
  • delete-old-versions - delete excess backup files without asking.
(setq make-backup-files t)
(setq vc-make-backup-files t)
(setq backup-by-copying t)
(setq version-control t)
(setq delete-old-versions t)
(setq kept-old-versions 6)
(setq kept-new-versions 9)

auto-save files use hashmarks (#) and shall be written locally within the project directory (along with the actual files). The reason is that auto-save files are just temporary files that Emacs creates until a file is saved again. auto-save files are created whenever Emacs crashes.

We disable the auto-save mode and use super-save package instead - it auto-saves buffers, when certain events happen - e.g. switch between buffers, an Emacs frame loses focus, etc. It’s both something that augments and replaces the standard auto-save-mode.

(setq auto-save-default nil)

(use-package super-save
  :straight t
  :config
  (setq super-save-auto-save-when-idle t)
  (setq super-save-idle-duration 15)
  (setq super-save-remote-files nil)
  (super-save-mode +1))

Undo and redo

The changes of buffer are also saved to a file, so this works between Emacs restarts as well.

(use-package undo-tree
  :straight t
  :init
  (setq undo-tree-visualizer-timestamps t)
  (setq undo-tree-visualizer-diff t)
  (setq undo-tree-auto-save-history t)
  :config
  (global-undo-tree-mode +1))

Recently visited files

TODO

Candidate selection and search methods

Completion framework

Completion framework refers to searching, narrowing and selecting a candidate from multiple alternatives.

Orderless completion style

A completion style is a back-end for completion. Probably the most powerful completion style, that combines multiple different styles, is orderless. The orderless project page has extensive documentation.

In orderless completion style, a search pattern is split into components (check orderless-component-separator). Each of these components can be matched using a different matching style. It’s possible to force a particular matching style for a given component (check orderless-style-dispatchers) to have more fine-grained control.

;;; j-orderless.el --- Extensions for orderless -*- lexical-binding: t -*-

;;; Commentary:
;;
;; Extensions for orderless.

;;; Code:

(defgroup j-orderless ()
  "Extensions for orderless."
  :group 'minibuffer)

(defcustom j-orderless-default-styles
  '(orderless-flex
    orderless-strict-leading-initialism
    orderless-regexp
    orderless-prefixes
    orderless-literal)
  "List that should be assigned to `orderless-matching-styles'."
  :type 'list
  :group 'j-orderless)

(defun j-orderless-literal-dispatcher (pattern _index _total)
  "Literal style dispatcher using the equals sign as a suffix."
  (when (string-suffix-p "=" pattern)
    `(orderless-literal . ,(substring pattern 0 -1))))

(defun j-orderless-initialism-dispatcher (pattern _index _total)
  "Leading initialism dispatcher using the comma as a suffix."
  (when (string-suffix-p "," pattern)
    `(orderless-strict-leading-initialism . ,(substring pattern 0 -1))))

(provide 'j-orderless)
;;; j-orderless.el ends here

Load j-orderless package.

(use-package j-orderless
  :straight (:type built-in)
  :demand t)
(use-package orderless
  :straight t
  :after j-orderless
  :config
  (setq orderless-component-separator " +")
  (setq orderless-matching-styles j-orderless-default-styles)
  (setq orderless-style-dispatchers
        '(j-orderless-literal-dispatcher
          j-orderless-initialism-dispatcher))
  :bind
  (:map minibuffer-local-completion-map
	("SPC" . nil)
	("?" . nil)))

Minibuffer

The minibuffer is where commands read input such as file names, search strings, buffer names, etc. When the input string is being typed in the minibuffer Emacs can fill in the rest, or some of it. When the completion is available, some keys can be bound to commands that try to complete the mininbuffer input.

There are multiple completion styles used in the minibuffer:

  • orderless which is described in the previous section;
  • partial-completion which is built-in and provides completions for file system paths e.g. by typing ~/.l/s/fo we get ~/.local/share/fonts;
  • substring maps foobar with point between foo and bar as .*foo.*bar.*;
  • flex maps abc as a.*b.*c.

The minibuffer history is saved using the built-in mechanism (see section TODO).

While typing input into the minibuffer, the *Completions* buffer is shown (with all possible candidates) by pressing tab key. This behaviour is supressed by setting minibuffer-auto-help to nil as we use Embark instead (see section TODO).

;;; j-minibuffer.el --- Extensions for minibuffer -*- lexical-binding: t -*-

;;; Commentary:
;;
;; Extensions for minibuffer.

;;; Code:

(defgroup j-minibuffer ()
  "Extensions for minibuffer."
  :group 'minibuffer)

(defcustom j-minibuffer-completions-regexp
  "\\*\\(Completions\\|Embark Collect \\(Live\\|Completions\\)\\)"
  "Regexp to match window names with completion candidates.
Used by `j-minibuffer--get-completions'."
  :group 'j-minibuffer
  :type 'string)

;;;###autoload
(defun j-minibuffer-focus-mini ()
  "Focus the active minibuffer."
  (interactive)
  (let ((mini (active-minibuffer-window)))
    (when mini
      (select-window mini))))

(defun j-minibuffer--get-completions ()
  "Find completions buffer."
  (get-window-with-predicate
   (lambda (window)
     (string-match-p
      j-minibuffer-completions-regexp
      (format "%s" window)))))

;;;###autoload
(defun j-minibuffer-focus-mini-or-completions ()
  "Focus the active minibuffer or completions buffer."
  (interactive)
  (let* ((minibuffer (active-minibuffer-window))
         (completions (j-minibuffer--get-completions)))
    (cond ((and minibuffer (not (minibufferp)))
           (select-window minibuffer nil))
          ((and completions (not (eq (selected-window) completions)))
           (select-window completions nil)))))

(provide 'j-minibuffer)
;;; j-minibuffer.el ends here

Load j-minibuffer package.

(use-package j-minibuffer
  :straight (:type built-in)
  :demand t)
(use-package minibuffer
  :after j-minibuffer
  :config
  (setq completion-styles '(partial-completion substring flex orderless))
  (setq completion-category-defaults nil)
  (setq completion-cycle-threshold nil)
  (setq completion-flex-nospace nil)
  (setq completion-pcm-complete-word-inserts-delimiters t)
  (setq completion-pcm-word-delimiters "-_./:| ")
  (setq completion-show-help nil)
  (setq completion-auto-help nil)
  (setq completion-ignore-case t)
  (setq-default case-fold-search t)
  (setq completions-format 'one-column)
  (setq completions-detailed t)
  (setq read-buffer-completion-ignore-case t)
  (setq read-file-name-completion-ignore-case t)
  (setq enable-recursive-minibuffers t)
  (setq read-answer-short t)
  (setq resize-mini-windows t)
  (setq minibuffer-eldef-shorten-default t)
  (file-name-shadow-mode +1)
  (minibuffer-depth-indicate-mode +1)
  (minibuffer-electric-default-mode +1)
  :bind
  (("s-f" . find-file)
   ("s-F" . find-file-other-window)
   ("s-b" . switch-to-buffer)
   ("s-B" . switch-to-buffer-other-window)
   ("s-d" . dired)
   ("s-D" . dired-other-window)
   ("s-v" . j-minibuffer-focus-mini-or-completions)))

Annotations for completion candidates

The marginalia package provides annotations for completion candidates in vertical view.

(use-package marginalia
  :straight t
  :config
  (setq marginalia-annotators
	'(marginalia-annotators-heavy
	  marginalia-annotators-light))
  (marginalia-mode +1))

Enhanced minibuffer commands

The consult package enhances various commands that are meant to replace the existing ones. The consult commands offer an improved interactive experience, can add live previews, filtering and narrowing. It is achieved by creating a wrapper for completing-read function.

(use-package consult
  :straight t
  :config
  (setq consult-line-numbers-widen t)
  (setq completion-in-region-function #'consult-completion-in-region)
  (setq consult-async-min-input 3)
  (setq consult-narrow-key ">")
  :bind
  (("s-y" . consult-yank)
   ("C-x M-:" . consult-complex-command)
   ("C-x M-m" . consult-minor-mode-menu)
   ("C-x M-k" . consult-kmacro)
   ("M-g g" . consult-goto-line)
   ("M-g M-g" . consult-goto-line)
   ("M-X" . consult-mode-command)
   ("M-K" . consult-keep-lines) ; M-S-k is similar to M-S-5 (M-%)
   ("M-F" . consult-focus-lines) ; same principle
   ("M-s g" . consult-grep)
   ("M-s m" . consult-mark)
   ("C-x r r" . consult-register) ; Use the register's prefix
   ("C-x r S" . consult-register-store)
   ("C-x r L" . consult-register-load)
   (:map consult-narrow-map
	 ("?" . consult-narrow-help))))

Extended actions

embark provides the ability to execute an action on a target using embark-act command. The target can be a completion candidate in the minibuffer, a region, symbol or URL at point, etc. The action is dependent on the type of the target. More precisely, there are multiple actions associated with a target (defined in an action keymap for the given target type) to choose from.

Embark acts both on individual targets and a set of candidates - for example the set of buffer candidates in minibuffer when switching to a different buffer. Embark can produce a buffer with the list of the current candidate set using embark-collect-snapshot. Similarly it can produce live/updating view of the current candidate set using embark-collect-live. The embark-export tries to open an appropriate buffer for the list of candidates (dired for list of files, ibuffer for list of buffers, etc.).

The “live” candidate view is used as the front-end for minibuffer (check minibuffer-setup-hook). This buffer pops up only after there is some input typed.

The j-embark enhances the embark package: TODO

(use-package embark
  :straight t
  :config
  (setq embark-collect-initial-view-alist
        '((kill-ring . zebra)
          (t . list)))
  (setq embark-action-indicator
	(lambda (map _target)
	  (which-key--show-keymap "Act" map nil nil 'no-paging)
	  #'which-key--hide-popup-ignore-command))
  (setq embark-become-indicator
	(lambda (map)
	  (which-key--show-keymap "Become" map nil nil 'no-paging)
	  #'which-key--hide-popup-ignore-command))
  :hook
  ((minibuffer-setup . embark-collect-completions-after-input)
   (embark-post-action . embark-collect--update-linked))
  :bind
  (("C-," . embark-act)
   (:map minibuffer-local-completion-map
	 ("C-," . embark-act)
	 ("C->" . embark-become)
	 ("M-q" . embark-collect-toggle-view))
   (:map embark-collect-mode-map
	 ("C-," . embark-act)
	 ("M-q" . embark-collect-toggle-view))))
;;; j-embark.el --- Extensions for embark -*- lexical-binding: t -*-

;;; Commentary:
;;
;; Extensions for embark.

;;; Code:

(when (featurep 'embark)
  (require 'embark))
(require 'j-common)
(require 'j-minibuffer)

(defgroup j-embark ()
  "Extensions for embark."
  :group 'editing)

(defun j-embark--live-buffer-p ()
  "Determine presence of a linked live occur buffer."
  (let ((buf embark-collect-linked-buffer))
    (when buf
      (window-live-p (get-buffer-window buf)))))

(defun j-embark--live-completions-p ()
  "Determine whether current collection is for live completions."
  (and (derived-mode-p 'embark-collect-mode)
       (eq embark-collect--kind :completions)))

;;;###autoload
(defun j-embark-collect-fit-window (&rest _)
  "Fit Embark's collect completions window to its buffer.
To be added to `embark-collect-post-revert-hook'."
  (when (derived-mode-p 'embark-collect-mode)
    (fit-window-to-buffer (get-buffer-window)
                          (round (* 0.30 (frame-height))) 1)))

;;;###autoload
(defun j-embark-completions-toggle ()
  "Toggle `embark-collect-completions'."
  (interactive)
  (cond ((j-embark--live-buffer-p)
	 (kill-buffer embark-collect-linked-buffer))
	((j-embark--live-completions-p)
	 (kill-buffer)
	 (select-window (active-minibuffer-window)))
	(t (embark-collect-completions))))

;;;###autoload
(defun j-embark-keyboard-quit ()
  "Control the exit behaviour for Embark collect buffers.

If in a live Embark collect/completions buffer and unless the
region is active, run `abort-recursive-edit'.  Otherwise run
`keyboard-quit'.

If the region is active, deactivate it.  A second invocation of
this command is then required to abort the session.

This is meant to be bound in `embark-collect-mode-map'."
  (interactive)
  (if (j-embark--live-completions-p)
      (if (use-region-p)
          (keyboard-quit)
        (kill-buffer)
        (abort-recursive-edit))
    (keyboard-quit)))

;;;###autoload
(defun j-embark-next-line-or-mini (&optional arg)
  "Move to the next line or switch to the minibuffer.
This performs a regular motion for optional ARG lines, but when
point can no longer move in that direction, then it switches to
the minibuffer."
  (interactive "p")
  (if (or (eobp) (eq (point-max) (save-excursion (forward-line 1) (point))))
      (j-minibuffer-focus-mini)    ; from `j-minibuffer.el'
    (forward-line (or arg 1)))
  (setq this-command 'next-line))

;;;###autoload
(defun j-embark-previous-line-or-mini (&optional arg)
  "Move to the next line or switch to the minibuffer.
This performs a regular motion for optional ARG lines, but when
point can no longer move in that direction, then it switches to
the minibuffer."
  (interactive "p")
  (let ((num (j-common-number-negative arg))) ; from `j-common.el'
    (if (bobp)
        (j-minibuffer-focus-mini)    ; from `j-minibuffer.el'
      (forward-line (or num 1)))))

(defun j-embark--switch-to-completions ()
  "Subroutine for switching to the Embark completions buffer."
  (unless (j-embark--live-buffer-p)
    (j-embark-completions-toggle))
  (let ((win (get-buffer-window embark-collect-linked-buffer)))
    (select-window win)))

;;;###autoload
(defun j-embark-switch-to-completions-top ()
  "Switch to the top of Embark's completions buffer.
Meant to be bound in `minibuffer-local-completion-map'."
  (interactive)
  (j-embark--switch-to-completions)
  (goto-char (point-min)))

;;;###autoload
(defun j-embark-switch-to-completions-bottom ()
  "Switch to the bottom of Embark's completions buffer.
Meant to be bound in `minibuffer-local-completion-map'."
  (interactive)
  (j-embark--switch-to-completions)
  (goto-char (point-max))
  (forward-line -1)
  (goto-char (point-at-bol))
  (recenter
   (- -1
      (min (max 0 scroll-margin)
           (truncate (/ (window-body-height) 4.0))))
      t))

(provide 'j-embark)
;;; j-embark.el ends here

Load j-embark package.

(use-package j-embark
  :straight (:type built-in)
  :after embark
  :hook
  (embark-collect-post-revert . j-embark-collect-fit-window)
  :bind
  (:map embark-collect-mode-map
	("h" . helpful-at-point)
	("C-g" . j-embark-keyboard-quit)
	("C-n" . j-embark-next-line-or-mini)
	("C-p" . j-embark-previous-line-or-mini)
	("C-l" . j-embark-completions-toggle))
  (:map minibuffer-local-completion-map
	("C-n" . j-embark-switch-to-completions-top)
	("C-p" . j-embark-switch-to-completions-bottom)
	("C-l" . j-embark-completions-toggle)))

TODO

(use-package embark-consult
  :straight t
  :after (embark consult)
  :bind
  (:map embark-collect-mode-map
	("C-j" . embark-consult-preview-at-point)))

Projects

A “project” is a version controlled directory governed by a program (such as git). The project.el has commands (C-x p prefix) that are useful for working with projects:

  • switching to a project;
  • executing a command in a project;
  • starting a shell in project root;
  • find file, search matches (regexp) in the current project;
  • etc.

Using any of the commands listed in C-x p C-h will append the current project to a list of known projects, stored in the dynamically updated project--list variable, whose contents are stored in a file defined by project-list-file.

Search commands

Search, replace, occur and grep

isearch and replace are built-in into Emacs. Their main functionality includes:

  • incremental search forward/backward;
  • search and replace a matched string with another string;
  • listing all matches (string or regular expression) into a separate buffer (occur-mode).

The most common key bindings:

Key bindingDescription
C-ssearch forward
C-rsearch backward
C-M-ssearch regexp forward
C-M-rsearch regexp backward
M-%replace string matches
C-M-%replaces regexp matches
C-s M-rtoggle regexp search
M-s olist all regexp matches in a separate buffer
C-s C-wsearch char or word at point
M-s .search symbol at point
M-s h rhighlight regexp
M-s h uhighlight undo
C-h k C-sshow help with additional keybindings

The occur and replace operations are aware of active region, so the search and replace operation is executed only in the highlighted area (also possible to be done with narrowing C-x n ...).

Due to the combined effect of the values assigned to the variables search-whitespace-regexp, isearch-lax-whitespace, isearch-regexp-lax-whitespace, the space is interpreted as wildcard (a b c as search input is interpreted as a.*b.*c.*). This affects string search, but not the regexp search. To interprate space as literal, toggle whitespace matching with M-s SPC.

(use-package isearch
  :config
  (setq search-highlight t)
  (setq search-whitespace-regexp ".*?")
  (setq isearch-lax-whitespace t)
  (setq isearch-regexp-lax-whitespace nil)
  (setq isearch-lazy-highlight t)
  (setq isearch-lazy-count t)
  (setq lazy-count-prefix-format nil)
  (setq lazy-count-suffix-format " (%s/%s)")
  (setq isearch-yank-on-move 'shift)
  (setq isearch-allow-scroll 'unlimited)
  :bind
  ((:map minibuffer-local-isearch-map
	 ("M-/" . isearch-complete-edit))
   (:map isearch-mode-map
	 ("C-g" . isearch-cancel)
	 ("M-/" . isearch-complete))))

The occur-mode buffer can be changed to editable (occur-edit-mode) by pressing e. To switch back from editable buffer, use C-c C-c.

(use-package replace
  :config
  (setq list-matching-lines-jump-to-current-line t)
  :hook
  ((occur-mode . hl-line-mode)
   (occur-mode . (lambda () (toggle-truncate-lines t))))
  :bind
  (("M-s M-o" . multi-occur)
   (:map occur-mode-map
	 ("t" . toggle-truncate-lines))))
(use-package j-search
  :straight (:type built-in)
  :bind
  (("M-s %" . j-search-isearch-replace-symbol)
   ("M-s M-<" . j-search-isearch-beginning-of-buffer)
   ("M-s M->" . j-search-isearch-end-of-buffer)
   ("M-s g" . j-search-grep)
   ("M-s u" . j-search-occur-urls)
   ("M-s M-u" . j-search-occur-browse-url)
  (:map isearch-mode-map
    ("<backspace>" . j-search-isearch-abort-dwim)
    ("<C-return>" . j-search-isearch-other-end))))
;;; j-search.el --- Extensions for search, replace and grep -*- lexical-binding: t -*-

;;; Commentary:
;;
;; Extensions for search, replace and grep.

;;; Code:

(require 'isearch)
(require 'replace)
(require 'grep)

;;;; Isearch

;;;###autoload
(defun j-search-isearch-other-end ()
  "End current search in the opposite side of the match.
Particularly useful when the match does not fall within the
confines of word boundaries (e.g. multiple words)."
  (interactive)
  (isearch-done)
  (when isearch-other-end
    (goto-char isearch-other-end)))

;;;###autoload
(defun j-search-isearch-abort-dwim ()
  "Delete failed `isearch' input, single char, or cancel search.

This is a modified variant of `isearch-abort' that allows us to
perform the following, based on the specifics of the case: (i)
delete the entirety of a non-matching part, when present; (ii)
delete a single character, when possible; (iii) exit current
search if no character is present and go back to point where the
search started."
  (interactive)
  (if (eq (length isearch-string) 0)
      (isearch-cancel)
    (isearch-del-char)
    (while (or (not isearch-success) isearch-error)
      (isearch-pop-state)))
  (isearch-update))

(defmacro j-search-isearch-occurrence (name edge &optional doc)
  "Construct function for moving to `isearch' occurrence.
NAME is the name of the function.  EDGE is either the beginning
or the end of the buffer.  Optional DOC is the resulting
function's docstring."
  `(defun ,name (&optional arg)
     ,doc
     (interactive "p")
     (let ((x (or arg 1))
           (command (intern (format "isearch-%s-of-buffer" ,edge))))
       (isearch-forward-symbol-at-point)
       (funcall command x))))

(j-search-isearch-occurrence
 j-search-isearch-beginning-of-buffer
 "beginning"
 "Run `isearch-beginning-of-buffer' for the symbol at point.
With numeric ARG, move to ARGth occurrence counting from the
beginning of the buffer.")

(j-search-isearch-occurrence
 j-search-isearch-end-of-buffer
 "end"
 "Run `isearch-end-of-buffer' for the symbol at point.
With numeric ARG, move to ARGth occurrence counting from the
end of the buffer.")

;;;; Replace/Occur

;; TODO: make this work backwardly when given a negative argument
(defun j-search-isearch-replace-symbol ()
  "Run `query-replace-regexp' for the symbol at point."
  (interactive)
  (isearch-forward-symbol-at-point)
  (isearch-query-replace-regexp))

(defvar j-search-url-regexp
  (concat
   "\\b\\(\\(www\\.\\|\\(s?https?\\|ftp\\|file\\|gopher\\|"
   "nntp\\|news\\|telnet\\|wais\\|mailto\\|info\\):\\)"
   "\\(//[-a-z0-9_.]+:[0-9]*\\)?"
   (let ((chars "-a-z0-9_=#$@~%&*+\\/[:word:]")
	     (punct "!?:;.,"))
     (concat
      "\\(?:"
      ;; Match paired parentheses, e.g. in Wikipedia URLs:
      ;; http://thread.gmane.org/47B4E3B2.3050402@gmail.com
      "[" chars punct "]+" "(" "[" chars punct "]+" ")"
      "\\(?:" "[" chars punct "]+" "[" chars "]" "\\)?"
      "\\|"
      "[" chars punct "]+" "[" chars "]"
      "\\)"))
   "\\)")
  "Regular expression that matches URLs.
Copy of variable `browse-url-button-regexp'.")

(autoload 'goto-address-mode "goto-addr")

;;;###autoload
(defun j-search-occur-urls ()
  "Produce buttonised list of all URLs in the current buffer."
  (interactive)
  (add-hook 'occur-hook #'goto-address-mode)
  (occur j-search-url-regexp "\\&")
  (remove-hook 'occur-hook #'goto-address-mode))

;;;###autoload
(defun j-search-occur-browse-url ()
  "Point browser at a URL in the buffer using completion.
Which web browser to use depends on the value of the variable
`browse-url-browser-function'.

Also see `j-search-occur-url'."
  (interactive)
  (let ((matches nil))
    (save-excursion
      (goto-char (point-min))
      (while (search-forward-regexp j-search-url-regexp nil t)
        (push (match-string-no-properties 0) matches)))
    (funcall browse-url-browser-function
             (completing-read "Browse URL: " matches nil t))))

;;;; Grep

(defvar j-search--grep-hist '()
  "Input history of grep searches.")

;;;###autoload
(defun j-search-grep (regexp &optional recursive)
  "Run grep for REGEXP.

Search in the current directory using `lgrep'.  With optional
prefix argument (\\[universal-argument]) for RECURSIVE, run a
search starting from the current directory with `rgrep'."
  (interactive
   (list
    (read-from-minibuffer "Local grep for PATTERN: "
				          nil nil nil 'j-search--grep-hist)
    current-prefix-arg))
  (unless grep-command
    (grep-compute-defaults))
  (if recursive
        (rgrep regexp "*" default-directory)
    (lgrep regexp "*" default-directory)
  (add-to-history 'j-search--grep-hist regexp)))

(provide 'j-search)
;;; j-search.el ends here

Writable grep buffer

The search result of grep is placed into a separate buffer (similar to occur). In order to edit this buffer, wgrep package is necessary.

(use-package wgrep
  :straight t
  :config
  (setq wgrep-auto-save-buffer t)
  (setq wgrep-change-readonly-file t)
  :bind
  (:map grep-mode-map
	("e" . wgrep-change-to-wgrep-mode)
	("C-x C-q" . wgrep-change-to-wgrep-mode)
	("C-c C-c" . wgrep-finish-edit)))

Cross-references

An identifier is a syntactical unit of a program: a function, a class, a data type, etc. Software development requires quickly looking up identifiers, their definitions, renaming them across the project, etc. xref provides an interface for these capabilities. The backend for xref is major-mode specific, it can be built-in (for elisp) or it can be an external program such as etags that comes with Emacs.

(use-package xref
  :config
  ;; M-.
  (setq xref-show-definitions-function #'xref-show-definitions-completing-read)
  ;; grep and the like
  (setq xref-show-xrefs-function #'xref-show-definitions-buffer)
  (setq xref-file-name-display 'project-relative)
  (setq xref-search-program 'ripgrep))

Directory, buffer and window management

Working with buffers

Unique names

If there are mutliple files open with the same name, Emacs will append a number such as <1>, <2>, etc. to distinguish them. uniquify makes the buffer names unique by adding just enough path of the file.

(use-package uniquify
  :config
  (setq uniquify-buffer-name-style 'forward)
  (setq uniquify-strip-common-suffix t)
  (setq uniquify-after-kill-buffer-p t))

Window configuration

Buffers are displayed by calling display-buffer function. It performs several steps in order to find a window for displaying a buffer. These steps are referred to as display actions. This list of actions is traversed one by one until a display action returns a window to display buffer in (if it can’t return the window, nil is returned).

The display action is composed of display action functions and display action alist. The display-action function accepts two arguments: the buffer to be displayed and the display action alist. For example:

(display-buffer
 (get-buffer-create "foo")                  ; buffer
 '((display-buffer-below-selected display-buffer-at-bottom) ; display action functions
   (inhibit-same-window . t)                ; display action alist
   (window-height . fit-window-to-buffer))) ; display action alist

The display actions are taken from these sources (from highest to lowest priority):

  • display-buffer-overriding-action (variable);
  • display-buffer-alist (user option);
  • action (argument);
  • display-buffer-base-action (user option);
  • display-buffer-fallback-action (constant).

Note that switching to a buffer (C-x b) calls switch-to-buffer, which normally just uses low-level routine set-buffer where the entries of display-buffer-alist are irrelevant. There are also occassions when switch-to-buffer calls pop-to-buffer. In that case the display-buffer-alist entries are relevant. To make switch-to-buffer behave according to display actions, there is the option switch-to-buffer-obey-display-actions.

Some commands that pop up a new window don’t focus the new window. There is a macro in j-window. It is a thin wrapper around display-buffer-* functions that makes the newly created window focused.

Load j-window package.

(use-package j-window
  :straight (:type built-in)
  :demand t)

The behaviour of how windows are displayed is controlled by user option display-buffer-alist.

(use-package window
  :after j-window
  :config
  (setq display-buffer-alist
	'(;; top side window
	  ("^\\*Apropos\\*"
	   (j-display-buffer-in-side-window-and-select)
	   (side . top)n
	   (slot . -1)
	   (window-height . 0.40))
	  ("^\\*[Hh]elp.*"
	   (j-display-buffer-in-side-window-and-select)
	   (side . top)
	   (slot . 0)
	   (window-height . 0.40))
	  ("^\\*Messages\\*"
	   (display-buffer-in-side-window)
	   (side . top)
	   (slot . 0)
	   (window-height . 0.40))
	  ("^\\*\\(Backtrace\\|Warnings\\|Compile-Log\\)\\*"
           (display-buffer-in-side-window)
           (side . top)
           (slot . 1)
           (window-height . 0.40)
           (window-parameters . ((no-other-window . t))))
	  ;; bottom side window
	  ("^\\*\\(Embark\\)?.*Completions.*"
           (display-buffer-in-side-window)
	   (window-height . 0.25)
           (side . bottom)
           (slot . 0)
           (window-parameters . ((no-other-window . t)
                                 (mode-line-format . none))))
	  ;; bottom window
	  ("^\\*.*\\(e?shell\\|v?term\\).*"
           (j-display-buffer-reuse-mode-window-and-select
	    j-display-buffer-at-bottom-and-select)
           (window-height . 0.25))
	  ;; below current window
	  ("^\\*Calendar.*"
           (j-display-buffer-reuse-mode-window-and-select
	    j-display-buffer-below-selected-and-select)
           (window-height . shrink-window-if-larger-than-buffer))))
  (setq window-resize-pixelwise t)
  (setq window-combination-resize t)
  (setq window-sides-vertical nil)
  (setq switch-to-buffer-in-dedicated-window 'pop)
  (setq switch-to-buffer-obey-display-actions t)
  (setq help-window-select t)
  :bind
  (("s-n" . next-buffer)
   ("s-p" . previous-buffer)
   ("s-o" . other-window)
   ("s-2" . split-window-below)
   ("s-3" . split-window-right)
   ("s-0" . delete-window)
   ("s-1" . delete-other-windows)
   ("s-5" . delete-frame)
   ("C-x _" . balance-windows)
   ("C-x +" . balance-windows-area)
   ("s-q" . window-toggle-side-windows)))
;;; j-window.el --- Extensions for window -*- lexical-binding: t -*-

;;; Commentary:
;;
;; Extensions for window.

;;; Code:

(defgroup j-window ()
  "Extensions for window."
  :group 'windows)

(defmacro j-display-buffer-and-select (fun)
  "Macro to display buffer and select its window.
FUN is one of display-buffer-* functions."
  `(defun ,(intern (format "j-%s-and-select" fun)) (buffer alist)
     ,(format "Display BUFFER as `%s' and select its window." fun)
     (let ((window (,fun buffer alist)))
       (if window
	   (select-window window)
	 nil))))

(j-display-buffer-and-select display-buffer-reuse-window)

(j-display-buffer-and-select display-buffer-reuse-mode-window)

(j-display-buffer-and-select display-buffer-in-side-window)

(j-display-buffer-and-select display-buffer-at-bottom)

(j-display-buffer-and-select display-buffer-below-selected)

(provide 'j-window)
;;; j-window.el ends here

The popper package improves handling of windows. Based on the configuration, some windows are popups. They shouldn’t mess up the window layout, can be hidden and displayed easily.

Popups can be displayed based on the context (which can be the current directory, project, etc.). Depending on the context, some popups are visible in a given context and some aren’t. Note that the commands with universal argument prefix (C-u) are useful as well.

There is a great video on how to use this package.

(use-package popper
  :straight t
  :config
  (setq popper-display-control 'user)
  (setq popper-group-function #'popper-group-by-project)
  (setq popper-reference-buffers
	'("^\\*Apropos\\*"
	  "^\\*[Hh]elp.*"
	  "^\\*Messages\\*"
	  "^\\*\\(Backtrace\\|Warnings\\|Compile-Log\\)\\*"))
  (popper-mode +1)
  :bind
  (("C-." . popper-toggle-latest)
   ("M-." . popper-cycle)
   ("C-M-." . popper-toggle-type)))

Applications, utilities and major modes

Org mode

(defconst j-org-directory "~/org"
  "Directory with org-mode files.")

(defconst j-org-inbox-file
  (expand-file-name "inbox.org" j-org-directory)
  "File with captured and unprocessed TODO items.")

(defconst j-org-projects-file
  (expand-file-name "projects.org" j-org-directory)
  "File with project TODO items.")

(defconst j-org-actions-file
  (expand-file-name "actions.org" j-org-directory)
  "File with processed TODO items from inbox file.")

(defconst j-org-repeaters-file
  (expand-file-name "repeaters.org" j-org-directory)
  "File with TODO items that repeat (habits).")

(use-package org
  :straight (:type built-in)
  :config
  (setq org-directory j-org-directory)
  (setq org-imenu-depth 6)
  ;; General settings.
  (setq org-adapt-indentation nil)
  (setq org-special-ctrl-a/e nil)
  (setq org-special-ctrl-k nil)
  (setq org-M-RET-may-split-line '((default . nil)))
  (setq org-hide-emphasis-markers nil)
  (setq org-hide-macro-markers nil)
  (setq org-hide-leading-stars nil)
  (setq org-catch-invisible-edits 'show)
  (setq org-return-follows-link nil)
  (setq org-loop-over-headlines-in-active-region 'start-level)
  (setq org-cycle-separator-lines -1)
  (setq org-modules '(ol-info org-tempo org-habit))
  ;; Code blocks.
  (setq org-structure-template-alist
   '(("s" . "src")
     ("el" . "src emacs-lisp")
     ("sh" . "src shell")
     ("yaml" . "src yaml")
     ("toml" . "src toml")
     ("c" . "center")
     ("C" . "comment")
     ("e" . "example")
     ("q" . "quote")
     ("v" . "verse")))
  (setq org-confirm-babel-evaluate nil)
  (setq org-src-window-setup 'current-window)
  (setq org-edit-src-persistent-message nil)
  (setq org-src-fontify-natively t)
  (setq org-src-preserve-indentation t)
  (setq org-src-tab-acts-natively t)
  (setq org-edit-src-content-indentation 0)
  ;; Links.
  (setq org-link-keep-stored-after-insertion t)
  ;; Todo, tags.
  (setq org-todo-keywords
   '(;; TODO an item that needs addressing;
     ;; STARTED being addressed;
     ;; WAITING  dependent on something else happening;
     ;; DELEGATED someone else is doing it and I need to follow up with them;
     ;; ASSIGNED someone else has full, autonomous responsibility for it;
     ;; CANCELLED no longer necessary to finish;
     ;; DONE complete.
     (sequence "TODO(t)" "STARTED(s!)" "WAITING(w@/!)" "DELEGATED(e@/!)" "|"
               "ASSIGNED(a@/!)" "CANCELLED(c@/!)" "DONE(d!)")))
  (setq org-tag-alist
   '(;; ERRAND requires a short trip (deliver package, buy something);
     ;; CALL requires calling via phone, internet, etc.;
     ;; REPLY requires replying to an email;
     ;; VISIT requires a longer trip. ;; TODO
     (:startgroup . nil)
     ("ERRAND" . ?e) ("CALL" . ?c) ("REPLY" . ?r) ("VISIT" . ?v)
     (:endgroup . nil)))
  (setq org-highest-priority ?A)
  (setq org-lowest-priority ?C)
  (setq org-default-priority ?B)
  (setq org-fontify-done-headline nil)
  (setq org-fontify-quote-and-verse-blocks t)
  (setq org-fontify-whole-heading-line nil)
  (setq org-fontify-whole-block-delimiter-line nil)
  (setq org-enforce-todo-dependencies t)
  (setq org-enforce-todo-checkbox-dependencies t)
  (setq org-track-ordered-property-with-tag t)
  ;; Logs.
  (setq org-log-into-drawer t)
  (setq org-log-done 'time)
  (setq org-log-redeadline t)
  (setq org-log-reschedule t)
  (setq org-treat-insert-todo-heading-as-state-change nil)
  (setq org-read-date-prefer-future 'time)
  ;; Capture
  (setq org-default-notes-file j-org-inbox-file)
  (setq org-capture-templates
	`(("t" "TODO task" entry
	   (file j-org-inbox-file)
	   ,(concat "* TODO %?\n"
		    ":PROPERTIES:\n"
		    ":CREATED:  %U\n"
		    ":END:\n"))))
  ;; Agenda.
  (setq org-agenda-files
	(list j-org-inbox-file
              j-org-projects-file
              j-org-actions-file
              j-org-repeaters-file))
  (setq org-agenda-span 14)
  (setq org-agenda-start-on-weekday 1)
  (setq org-agenda-confirm-kill t)
  (setq org-agenda-show-all-dates t)
  (setq org-agenda-show-outline-path nil)
  (setq org-agenda-window-setup 'current-window)
  (setq org-agenda-restore-windows-after-quit t)
  (setq org-agenda-skip-comment-trees t)
  (setq org-agenda-menu-show-matcher t)
  (setq org-agenda-menu-two-columns nil)
  (setq org-agenda-sticky nil)
  (setq org-agenda-custom-commands-contexts nil)
  (setq org-agenda-max-entries nil)
  (setq org-agenda-max-todos nil)
  (setq org-agenda-max-tags nil)
  (setq org-agenda-max-effort nil)
  (setq org-agenda-block-separator ?—)
  (setq org-agenda-follow-indirect t)
  (setq org-agenda-include-deadlines t)
  (setq org-deadline-warning-days 14)
  (setq org-agenda-skip-scheduled-if-done nil)
  (setq org-agenda-skip-scheduled-if-deadline-is-shown t)
  (setq org-agenda-skip-timestamp-if-deadline-is-shown t)
  (setq org-agenda-skip-deadline-if-done nil)
  (setq org-agenda-skip-deadline-prewarning-if-scheduled 1)
  (setq org-agenda-skip-scheduled-delay-if-deadline nil)
  (setq org-agenda-skip-additional-timestamps-same-entry nil)
  (setq org-agenda-skip-timestamp-if-done nil)
  (setq org-agenda-search-headline-for-time t)
  (setq org-scheduled-past-days 365)
  (setq org-deadline-past-days 365)
  (setq org-agenda-time-leading-zero t)
  (setq org-agenda-current-time-string
        "Now -·-·-·-·-·-·-")
  (setq org-agenda-time-grid
        '((daily today require-timed)
          (0600 0700 0800 0900 1000 1100
                1200 1300 1400 1500 1600
                1700 1800 1900 2000 2100)
          " ....." "-----------------"))
  (setq org-agenda-custom-commands
   `(("d" "Daily schedule"
      ((agenda ""
               ((org-agenda-span 'day)
                (org-deadline-warning-days 14)))
       (todo "TODO"
             ((org-agenda-overriding-header
               "--- To refile -------------------------------")
              (org-agenda-files (list j-org-inbox-file))))
       (todo "TODO"
             ((org-agenda-overriding-header
               "--- Projects --------------------------------")
              (org-agenda-files (list j-org-projects-file))))
       (todo "TODO"
             ((org-agenda-overriding-header
               "--- Actions ---------------------------------")
              (org-agenda-files (list j-org-actions-file))
              (org-agenda-skip-function
               '(org-agenda-skip-entry-if 'deadline 'scheduled)))))
      ((org-agenda-compact-blocks t)
       (org-use-property-inheritance t)))))
  (setq org-refile-targets
	'((j-org-projects-file :maxlevel . 1)
	  (j-org-actions-file :level . 0)
	  (j-org-repeaters-file :level . 0)))
  ;; Refile.
  (setq org-refile-use-outline-path 'file)
  (setq org-outline-path-complete-in-steps nil)
  (setq org-refile-allow-creating-parent-nodes 'confirm)
  (setq org-refile-use-cache t)
  ;; Habit.
  (setq org-habit-preceding-days 10)
  (setq org-habit-following-days 5)
  (setq org-habit-graph-column 65)
  (setq org-habit-show-habits-only-for-today nil)

  :bind
  (("C-c a" . org-agenda)
   ("C-c c" . org-capture)
   ("C-c l" . org-store-link)
   (:map org-mode-map
         ("M-n" . outline-next-visible-heading)
         ("M-p" . outline-previous-visible-heading)
	 ("C-'" . nil)
	 ("C-," . nil)
	 ("<C-return>" . nil)
	 ("<C-S-return>" . nil)
         ("<C-M-return>" . org-insert-subheading))))
;   (:map org-agenda-mode-map
;	 ("n" . org-agenda-next-item)
;         ("p" . org-agenda-previous-item))))

General interface and interactions