Declaring Emacs Bankruptcy

I declared Emacs bankruptcy.

Not because I stopped using Emacs —I could not do that— but because the config had accumulated so many layers over the years that fixing one thing would break two others. Opening a new file caused a noticeable freeze, Org-roam completion worked sometimes. Grammar checking occasionally ate all the CPU. The theme had a visual glitch. Nothing catastrophic on its own. Everything catastrophic together.

So I rewrote it from scratch with two hard requirements: modular and fast.

The new one keeps config.el as a thin orchestrator and splits functionality into named modules:

modules/
  lr-macos.el       macOS-specific frame and system settings
  lr-ui.el          theme, modeline, dashboard
  lr-completion.el  Corfu, Vertico, Consult
  lr-editor.el      evil, editing behaviour
  lr-prog.el        LSP, flycheck, error navigation
  lr-tools.el       magit, dired, terminals
  lr-writing.el     ltex-ls grammar checker
  lr-org-core.el    org-mode base settings
  lr-org-roam.el    org-roam, vulpea, dailies
  lr-org-noter.el   pdf annotation
  lr-academic.el    citar, bibtex, references
  lr-email.el       mu4e
  lr-irc.el         circe

config.el loads the performance-critical modules immediately and defers the heavy ones:

;; Load immediately (all fast, no I/O at startup)
(require 'lr-macos)
(require 'lr-ui)
(require 'lr-completion)
(require 'lr-editor)
(require 'lr-prog)
(require 'lr-tools)
(require 'lr-writing)

;; Defer heavy modules until org is first needed
(with-eval-after-load 'org
  (require 'lr-org-core)
  (require 'lr-org-roam)
  (require 'lr-org-noter)
  (require 'lr-academic))

;; mu4e deferred until first use
(with-eval-after-load 'mu4e
  (require 'lr-email))

This alone cut visible startup time substantially. The org stack never loads until you open an org file or an idle timer fires.

The org-roam startup problem

This was the hardest bug to fix and the most instructive.

The symptom: opening any org file would freeze Emacs for several seconds. The stack trace (sent via pkill -SIGUSR2) showed:

font-lock-fontify-keywords-region
  → citar-org-activate          (font-lock function)
  → (require 'citar)
  → citar-org-roam-setup
  → org-roam-db-sync            ← here
  → org-roam-db-query
  → +org-roam-try-init-db-a     (Doom's lazy-init advice, fires on first query)
  → org-roam-db-sync again      ← second sync
  → find-file-noselect          (for every roam file)
  → vc-refresh-state
  → git subprocess per file     ← freeze

A font-lock function requiring citar triggered a full org-roam DB sync, which opened every file in the roam directory, which ran git status on each one, which blocked. citar-org-activate is a font-lock keyword handler, it runs during redisplay when you first view a file containing a citation. You never see it coming.

The fix: block org-roam-db-sync entirely except in two cases — when called interactively (M-x org-roam-db-sync) or when an idle timer explicitly allows it.

(defvar salih/--org-roam-allow-sync nil)

(defadvice! salih/org-roam-block-eager-sync-a (&rest _)
  :before-while #'org-roam-db-sync
  (or salih/--org-roam-allow-sync
      (called-interactively-p 'any)))

;; Safe sync after 5s of idle — Emacs is not busy, no UX impact
(run-with-idle-timer
 5 nil
 (lambda ()
   (when (featurep 'org-roam)
     (let ((salih/--org-roam-allow-sync t))
       (org-roam-db-sync)))))

Doom's +org-init-roam-h internally uses cl-letf to replace org-roam-db-sync with ignore during autosync setup. cl-letf bypasses nadvice entirely, so startup is already safe — the advice only catches the citar and lazy-init triggered syncs that happen later.

The iCloud symlink bug

All my org-roam notes live under ~/roam, but the actual files are in iCloud: ~/roam/main, ~/roam/references, etc. are symlinks pointing into /Library/Mobile Documents/com~apple~CloudDocs/sync/org/.

Doom Emacs sets find-file-visit-truename t. This means that when you open /roam/main/foo.org, Emacs sets buffer-file-name to the iCloud truename, not the ~/roam path.

org-roam-file-p checks whether file-truename(buffer-file-name) is under file-truename(org-roam-directory). org-roam-directory is /Users/l/roam. The iCloud truename is /Users/l/Library/Mobile Documents/.... These share no common prefix. org-roam-file-p returns nil for every file accessed via a symlink.

That silently broke DB updates on save, find-file hooks, and the [[roam:Title]] → [[id:...]] link replacement.

The fix: pre-compute the truenames of symlinked subdirectories and extend org-roam-file-p to return t when a file is under any of them.

(defvar salih/--roam-symlink-truenames nil)

(defun salih/--compute-roam-symlink-truenames ()
  (when (bound-and-true-p org-roam-directory)
    (let ((roam-dir (expand-file-name org-roam-directory)))
      (setq salih/--roam-symlink-truenames
            (delq nil
                  (mapcar (lambda (entry)
                            (let ((path (expand-file-name entry roam-dir)))
                              (when (and (file-directory-p path)
                                         (file-symlink-p path))
                                (file-truename path))))
                          (directory-files roam-dir nil "^[^.]")))))))

(defadvice! salih/org-roam-file-p-symlink-a (fn &optional file)
  :around #'org-roam-file-p
  (or (funcall fn file)
      (when salih/--roam-symlink-truenames
        (let ((path (expand-file-name
                     (or file (buffer-file-name (buffer-base-buffer))))))
          (and path
               (not (auto-save-file-name-p path))
               (not (backup-file-name-p path))
               (string-suffix-p ".org" path t)
               (cl-some (lambda (dir)
                          (string-prefix-p (file-name-as-directory dir) path))
                        salih/--roam-symlink-truenames))))))

And consult-ripgrep does not follow symlinks by default, so the org-roam full-text search was also missing all symlinked directories. Fixed by adding --follow to the ripgrep invocation.

The ltex-ls completion conflict

I use ltex-ls for grammar checking in writing modes. ltex-ls registers itself as an LSP client for org-mode by default (via lsp-ltex-active-modes).

When ltex-ls starts in an org buffer, lsp-mode adds lsp-completion-at-point to the buffer's completion-at-point-functions. ltex-ls does not actually support textDocument/completion, so every completion attempt produced a noisy error and silently displaced org-roam's completion functions from the capf list.

The initial instinct was to remove org-mode from the registered LSP client after it loaded:

;; This does not work
(when-let ((client (gethash 'ltex-ls lsp-clients)))
  (setf (lsp--client-major-modes client) ...))

setf with lsp--client-major-modes requires a generalized-variable setter that lsp-mode does not define for runtime use — it works at compile time but not inside with-eval-after-load blocks. The error message was void-function (setf lsp--client-major-modes).

The correct fix: set lsp-ltex-active-modes before lsp-ltex.el loads. lsp-register-client evaluates lsp-ltex-active-modes at load time to build the client's :major-modes list.

;; Must be set before lsp-ltex.el loads (top of lr-writing.el, before after! blocks)
(setq lsp-ltex-active-modes
      '(text-mode latex-mode LaTeX-mode markdown-mode gfm-mode
        org-mode   ; kept so SPC t G can start it manually; not in auto-start hook
        rst-mode message-mode mu4e-compose-mode))

The global-git-commit-mode error

On first file open, the doom-first-file-hook tried to enable global-git-commit-mode and crashed with void-variable function. The magit-commit library was being loaded via a mode-line hook before its dependencies were ready.

The fix is minimal: remove it from the first-file hook and wire it properly:

(remove-hook 'doom-first-file-hook #'global-git-commit-mode)
(with-eval-after-load 'git-commit
  (add-hook 'find-file-hook #'git-commit-setup-check-buffer)
  (add-hook 'after-change-major-mode-hook
            #'git-commit-setup-font-lock-in-buffer))

Full configuration is available at: github

It might be interesting to try out scala again now. Follow up on Emacs and Scala #Emacs #Programming