*article* init.el *Messages*

Notes on tooling

Emacs as an Editor and Development Environment

C++ and Rust, tree-sitter and LSP, and a config that earns its keep

-UUU:----F1 article.txt All L1 (Text Fill)

Emacs as an editor

Emacs is a Lisp interpreter with a text-editing library. Almost everything above its small C core is Elisp, a lisp dialect designed specifically for Emacs, that runs in the same environment and can be inspected and changed while the editor is running. If you don't like a behavior, you can look up what function a key runs (C-h k), read its source, and modify or replace it. Most editors simply don't support this type of behavior.

The Emacs interface is uniform. Everything is a buffer — source files, compilation output, shells, gdb sessions, magit (an Elisp git package) diffs, directory listings — and every buffer responds to the same movement, search, and editing commands. You learn one set of commands and use it everywhere. Second, the knowledge lasts. Emacs configs and key-bindings from decades ago still work. Editors that were popular ten years ago are gone along with the time spent learning them; Emacs skills haven't needed relearning.

The tradeoff is maintenance. You assemble and maintain the environment yourself, and sometimes that costs time. An example in my emacs config being the removal/replacement of a package that turned out to provide compilation-mode regexps.

Emacs as a development environment

For a long time, the real argument against Emacs for C++ was that IDEs built on compiler frontends understood the code and Emacs didn't. Tags files and regex-based tools failed on templates, overloads, and macros. Language Server Protocol (LSP), a JSON-RPC based protocol between editors/IDEs and language servers, ended that. Clangd (LSP server for C++) uses the same Clang parse as the build, and rust-analyzer is developed by the Rust project. Any editor that speaks LSP now gets the same semantic analysis an IDE has.

My setup divides the work across layers. Tree-sitter handles syntax: rust-ts-mode highlights from an incremental parse tree, which is fast (the grammar is compiled C, loaded from Fedora's libtree-sitter-rust) and stays correct in complex or incomplete code. The language servers handle semantics — types, references, diagnostics — asynchronously, so the editor doesn't block. Eglot routes server results into Emacs's built-in facilities rather than building its own UI: diagnostics go to flymake, navigation to xref, signatures to eldoc, completion to completion-at-point (which company displays). The result is that C++ and Rust behave identically: M-. jumps to definitions and M-g n steps through diagnostics in both.

Clangd flags matter for my kind of work. --background-index builds a project-wide index, so cross-file navigation and find-references work on large codebases. --clang-tidy shows static-analysis findings while you're editing instead of in a CI report later — relevant when an accidental copy in a hot path is a real bug. --header-insertion=iwyu adds the right includes as you go. Keeping classic c++-mode instead of the tree-sitter version is also correct: cc-mode's offset system is the only one that can express my preferred stroustrup-with-Allman-braces style.

The Rust side needs less configuration because the toolchain is more standardized: rust-analyzer plus rust-src for stdlib navigation, rustfmt on save, rust-compile's regexps for clickable cargo errors, and rust-gdb's pretty-printers so debugging shows Vec and Option as values rather than raw memory.

The productivity comes from the loop, not the feature list — edit, compile, jump to error, fix, recompile, with side trips into the debugger and git, all in one process with one set of keybindings.

M-x compile produces a buffer where errors are links, C-x C-g opens the debugger, C-x g opens magit. Magit in particular — hunk-level staging, interactive rebase as an editable buffer — is better than any standalone git interface. Avoiding application switches matters because each switch costs you your train of thought, not just the seconds it takes.

The configuration itself is managed like production software. straight.el, a next-generation package manager for Emacs, pins every package to a commit with a lockfile (straight-freeze-versions after a known-good change, thaw to roll back), the compilers and servers come from Fedora's package set, and after this cleanup every line in init.el has a documented purpose.

Weaknesses

Elisp is mostly single-threaded, so a badly behaved synchronous operation can freeze the UI; my jsonrpc logging suppression and GC settings exist to keep eglot off that path. The gdb interface works but looks and feels dated compared to a modern IDE debugger. Emacs is hard for newcomers to learn. And being your own integrator means you occasionally spend an evening debugging your editor.

The tradeoff is favorable: full control, no vendor dependency, identical behavior over SSH to a colo box as on my desktop, and a tool that absorbed both LSP and tree-sitter without requiring me to start over.

init.el — C++ and Rust setup

The full configuration below: straight.el bootstrap, package declarations, eglot and clangd flags, the rust-mode hook, keybindings, and the c++-mode Allman/Stroustrup offsets.

~/.emacs.d/init.el Emacs Lisp
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; init.el
;; Bootstrap straight.el
(defvar bootstrap-version)
(let ((bootstrap-file
       (expand-file-name
        "straight/repos/straight.el/bootstrap.el"
        user-emacs-directory))
      (bootstrap-version 7))
  (unless (file-exists-p bootstrap-file)
    (with-current-buffer
        (url-retrieve-synchronously
         "https://raw.githubusercontent.com/radian-software/straight.el/develop/install.el"
         'silent 'inhibit-cookies)
      (goto-char (point-max))
      (eval-print-last-sexp)))
  (load bootstrap-file nil 'nomessage))

;;====================================================================
(straight-use-package 'magit)
(straight-use-package 'eglot)
(straight-use-package 'company)
(straight-use-package 'rust-mode) ; for compilation mode highlights
(straight-use-package 'markdown-mode)
(add-hook 'after-init-hook #'global-company-mode)

;;====================================================================
;(use-package rust-mode
;  :straight t)

(use-package solarized-theme
  :straight (:host github :repo "bbatsov/solarized-emacs")
  :init
  ;; Set options BEFORE loading the theme
  (setq solarized-use-variable-pitch nil ; avoid font size changes
        solarized-high-contrast-mode-line nil
        solarized-distinct-fringe-background nil
        x-underline-at-descent-line t)
  :config
  (load-theme 'solarized-light t))

(use-package doom-themes
  :straight t
  :config
  ;; Global settings
  (setq doom-themes-enable-bold t)
  ;doom-themes-enable-italic nil)
  ;(load-theme 'doom-solarized-light t)
  ;(load-theme 'doom-bluloco-light t)
  ;; Optional extras
  (doom-themes-visual-bell-config) ; flash mode-line on error
  (doom-themes-org-config)) ; better org-mode fontification

;;====================================================================
;; What straight.el does differently:
;; Clones each package from source (GitHub) rather than downloading tarballs
;; Locks exact commits in ~/.emacs.d/straight/versions/default.el -- fully reproducible
;; No MELPA Stable vs MELPA priority conflicts; it always gets the latest from the repo's default branch
;; Dependencies are resolved from the recipe, not archive metadata, so no mismatched compat/transient situations
;; Useful commands once set up:
;; M-x straight-pull-all           ;; update all packages
;; M-x straight-freeze-versions    ;; write lockfile
;; M-x straight-thaw-versions      ;; restore from lockfile
;; M-x straight-rebuild-package    ;; recompile a single package
;; M-x package-refresh-contents
;; M-x package-reinstall RET transient RET
;; M-x find-library RET transient RET
;; M-x load-theme

;;====================================================================
;; Eglot
;; https://joaotavora.github.io/eglot/#Quick-Start
;;====================================================================
(with-eval-after-load 'eglot
  (setq eglot-autoshutdown t)
  (setq eglot-send-changes-idle-time 0.1)
  (setq eglot-extend-to-xref t)
  (add-to-list 'eglot-server-programs
               '((c-mode c++-mode) . ("clangd"
                                      "--header-insertion=iwyu"
                                      "--background-index"
                                      "--clang-tidy"
                                      "--completion-style=detailed"
                                      "--enable-config"))))
(add-hook 'c-mode-hook #'eglot-ensure)
(add-hook 'c++-mode-hook #'eglot-ensure)
(add-hook 'eglot-managed-mode-hook (lambda ()
                                      (eglot-inlay-hints-mode 0)
                                      (flymake-mode 1)))
;;(remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend)))

;; Performance
(fset #'jsonrpc--log-event #'ignore)
(setq eglot-events-buffer-config '(:size 0 format: full))
(setq gc-cons-percentage 0.2
      gc-cons-threshold 80000000)
; (setq clang-format-style "/home/wds/.clang-format")

;;====================================================================
;; rust
;;====================================================================
(defun my-rust-mode-setup ()
  (eglot-ensure)
  (require 'rust-compile) ; highlighting in compilation mode
  (add-hook 'before-save-hook
            (lambda () (when (eglot-managed-p) (eglot-format-buffer)))
            nil t)
  (setq-local compile-command "cargo build"))

(setq-default eglot-workspace-configuration
              '(:rust-analyzer (:check (:command "clippy"))))
;(eglot-inlay-hints-mode 1)) --> doesn't work, disables fontification
(add-hook 'rust-ts-mode-hook #'my-rust-mode-setup)

;;====================================================================
;; Keystrokes
;;====================================================================
(global-set-key (kbd "C-x g") 'magit-status)
(global-set-key (kbd "") 'beginning-of-buffer)
(global-set-key (kbd "") 'end-of-buffer)
(global-set-key (kbd "C-x C-g") 'gdb-many-windows)

(setq mouse-wheel-scroll-amount
      '(1
        ((shift) . hscroll)
        ((meta))
        ((control meta) . global-text-scale)
        ((control) . nil))) ;; disable mouse wheel scroll events

;; set using this sequence
;; c-set-style -> c-set-offset/c-offsets-alist -> setq-local c-basic-offset
(defun my-c++-mode-setup ()
  ;; base style first - resets c-offsets-alist
  (c-set-style "stroustrup")
  ;; overrides
  ;; braces on new line (Allman style)
  (c-set-offset 'substatement-open 0)
  (c-set-offset 'defun-open 0)
  (c-set-offset 'class-open 0)
  (c-set-offset 'inline-open 0)
  (c-set-offset 'brace-list-open 0)
  (c-set-offset 'inlambda 0)
  (c-set-offset 'lambda-intro-cont '+)
  (c-set-offset 'inexpr-statement 0)
  (c-set-offset 'case-label '+)
  (c-set-offset 'statement-case-open '+)
  (setq-local indent-tabs-mode nil)
  (setq-local c-basic-offset 4)
  (c-toggle-auto-hungry-state 1))

(add-hook 'c++-mode-hook #'my-c++-mode-setup) ; #' is a function reference
(add-to-list 'auto-mode-alist '("\\.h\\'" . c++-mode))
(add-to-list 'auto-mode-alist '("\\.rs\\'" . rust-ts-mode))

(setq ediff-window-setup-function #'ediff-setup-windows-plain)

; remove gdb annoying prompt at startup for debuginfod servers
(with-eval-after-load 'gdb-mi
  (advice-add 'y-or-n-p :around
              (lambda (orig-fun prompt &rest args)
                (if (string-match-p "debuginfod" prompt)
                    nil
                  (apply orig-fun prompt args)))))

(defun remove-trailing-whitespace ()
  (interactive)
  ;(replace-regexp "[[:space:]]*$" "")
  (delete-trailing-whitespace))

;; Keep normal mouse wheel scrolling enabled.
(mouse-wheel-mode 1)

(custom-set-variables
 ;; custom-set-variables was added by Custom.
 ;; If you edit it by hand, you could mess it up, so be careful.
 ;; Your init file should contain only one such instance.
 ;; If there is more than one, they won't work right.
 '(column-number-mode t)
 '(diff-switches "-u")
 '(transient-mark-mode t))
;; so ctrl-space highlights mark

;;; uncomment for CJK utf-8 support for non-Asian users
;; (require 'un-define)

(custom-set-faces
 ;; custom-set-faces was added by Custom.
 ;; If you edit it by hand, you could mess it up, so be careful.
 ;; Your init file should contain only one such instance.
 ;; If there is more than one, they won't work right.
 '(default ((t (:family "Cascadia Mono" :foundry "SAJA" :slant normal :weight regular :height 110 :width normal)))))