Skip to content

It is my Emacs configuration, which does not start with a .emacs file anymore... hail to init.el!

Notifications You must be signed in to change notification settings

MassimoLauria/dotemacs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Massimo’s Emacs configuration

This is the Emacs config of Massimo Lauria (C) 2009-2024.

This configuration is under massive and continue change, and things break every day. I just put it online so you can cut & paste whatever you may find useful.

This configuration assume a modern Emacs installation (>=24). If your Emacs is less recent than that, the configuration will revert to a minimal version.

Literate configuration: this Emacs configuration is on the way to be a literate configuration developed as an org-mode file. The plan is that the very document you are reading right now will be the main part of the configuration itself. At this point this is not true but I am putting in place the infrastructure for it. I will slowly move part of the main config inside this README.org file.

Bootstrapping the setup

The variable base-config-path keeps the path where the configuration is installed. I add that to the load-path, and we also add the path of 3rd parties packages, which are packages I keep in the repository because they are not on melpa (yet).

(setq 3rdparties-packages-path (concat base-config-path "3rdparties/"))

(add-to-list 'load-path base-config-path)
(add-to-list 'load-path 3rdparties-packages-path)

The best way to read Info files is in emacs. The client merges all info dir files in a single index. Therefore among other things I make the index to show my local info documents as well. For example my copy of “Structure and Interpretation of Computer Programs”.

(if (not (boundp 'Info-directory-list))
    (setq Info-directory-list nil))
(add-to-list 'Info-directory-list (concat base-config-path "/info"))

Adjust PATH environment variables

When Emacs is lauched as an app (on MacOSX) or from a gui command (in linux) there is often the chance that the running environment does not contain some environment variables or does not set them up appropriately. These functions allow me to care of such issues as long as they arise in my setup. I don’t claim any generality here.

First I have two functions to manage variables for the runtime environment where Emacs has been executed from.

(defun environment-variable-add-to-list (varname element &optional append)
  "Add ELEMENT from the list in the enviroment variable VARNAME.

VARNAME is considered as a list of elements like \"aa:bb:cc\". If
VARNAME is undefined of empty, it defines it. If ELEMENT is
already in the list, the function won't do anything.

There is no guarantee on the actual order of the elements in the
list."
  (let ((separator (if (eq system-type 'windows-nt) ";" ":"))
        tmplist)
    (if (getenv varname)
        (setq tmplist (split-string
                       (getenv varname)
                       separator)))
    (add-to-list 'tmplist element append 'string-equal)
    (setenv varname (mapconcat 'identity tmplist ":"))))

(defun environment-variable-rm-from-list (varname element)
  "Remove ELEMENT from the list in the enviroment variable VARNAME.

VARNAME is considered as a list of elements like \"aa:bb:cc\". If
ELEMENT is not in the list, the function won't do anything.

There is no guarantee on the actual order of the elements in the
list."
  (let ((separator (if (eq system-type 'windows-nt) ";" ":"))
        tmplist)
    (if (getenv varname)
        (setq tmplist (split-string
                       (getenv varname)
                       separator)))
    (setenv varname (mapconcat 'identity (remove element tmplist) ":"))))

I use this functions primarily to fix PATH.

(defun environment-add-path (newpath &optional append)
  "Add NEWPATH to the PATH environment variable and to exec-path,

Ignore if the path does not exists."
  (when (file-directory-p newpath)
    (add-to-list 'exec-path newpath append 'string-equal)
    (environment-variable-add-to-list "PATH" newpath append)))

Some useful paths to add to the environment. In particular tools like Cask, cabal, cargo, install their executables files in a corresponding hidden folder inside $HOME.

(environment-add-path "/usr/local/bin")                        ;; Homebrew  (MacOS)
(environment-add-path (concat (getenv "HOME") "/.cask/bin"))   ;; Cask      (for Elisp)
(environment-add-path (concat (getenv "HOME") "/.local/bin"))  ;; Local/bin (GNU/Linux)

Actually the right way to set system-wide exec-paths on Mac is to use `/etc/paths.d’ files. I pick up these paths as well.

(with-temp-buffer
  (condition-case nil
      (dolist (file (cons "/etc/paths" (directory-files "/etc/paths.d/" t)))
        (if (not (file-directory-p file))
            (insert-file-contents file)))
    (error nil))

  (dolist (path (split-string (buffer-string) "\n" t))
    (if (file-directory-p path)
        (environment-add-path path))))

Some personal information

Some basic personal information. I used to keep these in a separate file but I believe it does not achieve any further privacy.

;; ;; My personal stuff, that I don't want to share.
(setq user-full-name "Massimo Lauria")
(setq user-mail-address "[email protected]")
(setq user-organisation "Università degli Studi di Roma \"La Sapienza\"")
(setq user-mail-signature "\
Massimo Lauria
http://www.massimolauria.net

Sapienza University of Rome
Dipartimento di Scienze Statistiche
Piazzale Aldo Moro, 5
00185 Roma, Italy
")

Basic

Basic editing setup. For example start scrolling few lines from top of bottom.

(setq scroll-margin 8)

Localization/Internationalization

(setenv "LANG" "it_IT.UTF-8")

Keyboard

I have my keyboard setup in a minor mode. This is mostly for editing, movements and things like that. Commands for specific tools are setup in the appropriate sections.

;; My keyboard configuration
(use-package mxl-keyboard
  :commands (mxl-keyboard-mode mxl-keyboard-global-mode)
  :diminish ""
  :config
  (mxl-keyboard-global-mode)
  :demand t)

I often type C-x C-z by mistake, which hides the current Emacs frame via susped-frame. I hate it.

(global-unset-key (kbd "C-x C-z"))

Having previous-buffer and next-buffer on the arrow keys is a trap. I tend to use reach for these command automatically and then I find myself going around buffer until I find the one I was looking for. Too inefficient. I’d rather disable such keys and use some version of switch-to-buffer.

(global-unset-key (kbd "C-x <left>"))
(global-unset-key (kbd "C-x <right>"))
(global-unset-key (kbd "C-x C-<left>"))
(global-unset-key (kbd "C-x C-<right>"))

When I delete the rest of a line via C-k, I want it to remove the newline in the end as well. But only when the cursor is at the first column.

(setq kill-whole-line t)

Function keys: easy to reach commands that are not immediately related to editing.

(global-set-key [f1]  'help)
;; (global-set-key [f2] 'ispell-buffer)  ;; in the spellcheck setup
(global-set-key [f3]  'kmacro-start-macro-or-insert-counter)
(global-set-key [f4]  'kmacro-end-or-call-macro)

Text encoding is always UTF-8

I work mostly with UTF-8 file, Hence UTF-8 is the default coding for buffers.

(prefer-coding-system       'utf-8)
(set-default-coding-systems 'utf-8)
(set-terminal-coding-system 'utf-8)
(set-keyboard-coding-system 'utf-8)
(setq default-buffer-file-coding-system 'utf-8)

The same setting seems to be needed for the clipboard.

(setq x-select-request-type '(UTF8_STRING COMPOUND_TEXT TEXT STRING))
(set-clipboard-coding-system 'utf-8)

I often need to write greek or math symbols (e.g. α, ⊕), and since I know LaTeX it is convenient to use the same markup to insert them in regular text. Nevertheless it is inconvenient to have that on by default. I activate it when needed typing C-\ (toggle-input-method).

(setq default-input-method 'TeX)
(set-input-method nil)

Italians holidays, timezones and calendar names

For me it is useful to have the agenda to remind of italian holidays, especially if they corresponds to vacation days. I override the original values of these variables because I don’t care to know about holidays that do not affect me.

;; Non-religious holidays
(setq holiday-general-holidays
      '((holiday-fixed 1 1 "Capodanno")
        (holiday-fixed 3 8 "Giornata internazionale della donna")
        (holiday-fixed 5 1 "Festa dei Lavoratori")
        (holiday-fixed 4 25 "Liberazione dal Nazifascismo")
        (holiday-fixed 6 2 "Festa della Repubblica")))

;; Catholics holidays that induce vacations
(setq holiday-christian-holidays
     '((holiday-fixed 12 8 "Immacolata Concezione")
       (holiday-fixed 12 25 "Natale")
       (holiday-fixed 12 26 "Santo Stefano")
       (holiday-fixed 1 6 "Epifania")
       (holiday-easter-etc -52 "Giovedì grasso")
       (holiday-easter-etc -47 "Martedì grasso")
       (holiday-easter-etc   0 "Pasqua")
       (holiday-easter-etc   1 "Pasquetta")
       (holiday-fixed 8 15 "Ferragosto")
       (holiday-fixed 11 1 "Ognissanti")))

;; No other religious holidays induce vacation days in Italy.
(setq holiday-bahai-holidays nil)
(setq holiday-hebrew-holidays nil)
(setq holiday-islamic-holidays nil)

In Italy the weekly calendar starts from Monday, hence I set calendar-week-start-day accordingly. It is also nice to have the names of months and weekdays translated.

(setq calendar-week-start-day 1
      calendar-day-name-array ["Domenica" "Lunedì" "Martedì" "Mercoledì"
                               "Giovedì" "Venerdì" "Sabato"]
      calendar-month-name-array ["Gennaio" "Febbraio" "Marzo" "Aprile" "Maggio"
                                 "Giugno" "Luglio" "Agosto" "Settembre"
                                 "Ottobre" "Novembre" "Dicembre"])

Several packages (e.g. Org-mode) need to recognize what a weekday name or a month name is. Package parse-time provides this functionality, but it only knows about English words. I can add the Italian ones to parse-time-weekdays and parse-time-months when package parse-time is loaded. I also add the timezones.

(defconst parse-time-weekdays
  '(("dom" . 0) ("lun" . 1) ("mar" . 2) ("mer" . 3) ("gio" . 4) ("ven" . 5) ("sab" . 6)
    ("domenica" . 0) ("lunedì" .  1) ("martedì" . 2) ("mercoledì" . 3)
    ("giovedì" .  4) ("venerdì" . 5) ("sabato" .  6))
  "Italian weekdays to add to `parse-time-weekdays'.")


(defconst parse-time-months-ita
  '(("gen" . 1) ("feb" . 2) ("mar" . 3) ("apr" .  4) ("mag" .  5) ("giu" .  6)
    ("lug" . 7) ("ago" . 8) ("set" . 9) ("ott" . 10) ("nov" . 11) ("dic" . 12)
    ("gennaio" . 1)   ("febbraio" . 2) ("marzo" . 3)     ("aprile" . 4)
    ("maggio" . 5)    ("giugno" . 6)   ("luglio" . 7)    ("agosto" . 8)
    ("settembre" . 9) ("ottobre" . 10) ("novembre" . 11) ("dicembre" . 12))
  "Italian manths to add to `parse-time-months'.")

(use-package parse-time
  :config
  (setq parse-time-months   (append parse-time-months parse-time-months-ita))
  (setq parse-time-weekdays (append parse-time-weekdays parse-time-weekdays-ita))
  (add-to-list 'parse-time-zoneinfo  '("cet" 3600 t) t)  ;; Central European Time
  (add-to-list 'parse-time-zoneinfo  '("cest" 7200)  t)  ;; Central European Summer Time
  )

Appearance

Fonts

The most important visual setup for a text editor is the font. We set the default font. The symbol-font is the fallback needed for some math symbols. I use nerd-fonts for icons and symbols.

(setq default-font "DejaVu Sans Mono")
(setq symbol-font "DejaVu Sans Mono")

(set-face-attribute 'default nil
                    :family default-font
                    :width 'normal
                    :height 180)

(use-package nerd-icons
  :commands (nerd-icons-faicon nerd-icons-octicons)
  :custom
  (nerd-icons-font-family "Symbols Nerd Font Mono"))

(use-package nerd-icons-dired
  :hook
  (dired-mode . nerd-icons-dired-mode))

In GUI emacs we use a specific font family for the nerd icons. To make this nerd-fonts to work in terminal we need to setup a single merged font like the ones at https://www.nerdfonts.com. I setup alacritty to use DejaVu Sans Mono Nerd Font and nerd fonts work fine there too.

I like Emacs to open in a wide frame at the center of the screen, on startup, at least on these systems with floating window managers. No internal border except for a small fringe on the left side. I disable any other decorations. Text is more readable with some additional space between lines.

(setq initial-frame-alist '((top . 0.5)    ;; center vertical position
                            (left . 0.5))) ;; center horizontal position


(setq default-frame-alist `((height . 64)
                            (width . 120)
                            (tool-bar . nil)
                            (line-spacing . 0.2)
                            (internal-border-width . 0)
                            (border-width . 0)
                            (vertical-scroll-bars . nil)
                            (horizontal-scroll-bars . nil)
                            (left-fringe . 8)
                            (right-fringe . 0)
                            (tool-bar-lines . 0)
                            (menu-bar-line . 0)
                            ))

Hint: To discover the properties of some text on the screen the command C-u C-x = gives them all: font, style, font-lock, char code.

Theme

The color theme I usually go for is Zenburn, but now I am trying modus-vivendi for a while.

(defun mxl/tune-modus-theme ()
  "Make some changes to the theme"
  (custom-set-faces
   `(org-block-begin-line
     ((t
       (:slant italic
        :background ,(modus-themes-get-color-value 'bg-inactive)
        :foreground ,(modus-themes-get-color-value 'bg-active)))))))

(use-package modus-themes
  :init
  (setq modus-themes-disable-other-themes t
        modus-themes-italic-constructs t
	      modus-themes-bold-constructs nil
	      modus-themes-mixed-fonts t
	      modus-themes-variable-pitch-ui nil )  ;; basic customizations


  (setq modus-themes-org-blocks 'gray-background) ;; org source blocks


  (setq modus-themes-mode-line '(accented borderless) ;; customizations for
        modus-themes-region '(accented bg-only))      ;; version <4 shipped with
                                                      ;; with Emacs 29.1

  ;; change defaults in modus themes
  (setq modus-themes-common-palette-overrides
    '((border-mode-line-active unspecified)     ;; borderless mode-line (1)
      (border-mode-line-inactive unspecified)   ;; borderless mode-line (2)
	    (fg-region unspecified)))                 ;; syntax visible in region

  ;; change defaults in modus vivendi specifically
  (setq modus-vivendi-palette-overrides
    '((bg-region bg-cyan-subtle)                ;; region color
      (bg-mode-line-active bg-cyan-subtle)
	    (fg-heading-1 keyword)))                  ;; org-heading

  ;; change defaults in modus operandi specifically
  (setq modus-operandi-palette-overrides
    '((bg-region bg-cyan-intense)               ;; region color
      (bg-mode-line-active bg-cyan-intense)
	    (fg-heading-1 keyword)))                  ;; org-heading

  :config
  (add-hook 'modus-themes-after-load-theme-hook #'mxl/tune-modus-theme)
  (modus-themes-load-theme 'modus-vivendi)
  (global-set-key (kbd "M-<f5>") 'modus-themes-toggle))

Syntax/Spell checkers add decorations to the text to signal mistakes. The defaults decorations are either too intrusive or too faint, so I customize them.

  • Violet for spelling/grammar mistakes
  • Red for syntax mistakes in programming languages
  • Yellow for syntax/style warnings
  • Blue for syntax/style notes
(custom-set-faces
 '(flycheck-error ((t (:underline "Red"))))
 '(flycheck-warning ((t (:underline "Yellow"))))
 '(flycheck-info ((t nil)))
 '(flymake-error ((t (:underline "Red1"))))
 '(flymake-warning ((t (:underline "yellow"))))
 '(flymake-note ((t (:underline "deep sky blue"))))
 '(flyspell-duplicate ((t (:underline (:color "magenta" :style wave)))))
 '(flyspell-incorrect ((t (:underline (:color "magenta" :style wave)))))
 '(writegood-duplicates-face ((t nil)))
 '(writegood-passive-voice-face ((t (:underline (:color "magenta" :style wave)))))
 '(writegood-weasels-face ((t (:underline (:color "magenta" :style wave))))))

Modeline customization

We use Doom Emacs Modeline (see here). Doom Modeline is minimal, well organized and nice to look at (especially with icons enabled). This also makes obsolete other customizations hacks I did, that often were incompatible with other emacs setup.

Note that Doom Emacs Modeline requires Nerd fonts. They are already be in my setup.

(use-package doom-modeline
  :ensure t
  :init
  (setq doom-modeline-icon t)
  (setq doom-modeline-major-mode-icon t)
  (setq doom-modeline-buffer-state-icon t)
  (setq doom-modeline-enable-word-count t)
  (setq doom-modeline-buffer-encoding t)
  (setq doom-modeline-env-enable-python nil)
  (setq doom-modeline-bar-width 6)
  (doom-modeline-mode 1))

Sometimes I want to hide my mode-line, maybe in conjunction with something like olivetti-mode.

(defun mxl/toggle-mode-line ()
  "Hide/show mode-line in the current buffer"
  (interactive)
  (if (local-variable-p 'mode-line-format)
      (kill-local-variable 'mode-line-format)
    (setq-local mode-line-format nil))
  (force-mode-line-update))

(defun mxl/hide-mode-line ()
  "Hide mode-line in the current buffer"
  (interactive)
  (setq-local mode-line-format nil))

(defun mxl/show-mode-line ()
  "Hide mode-line in the current buffer"
  (interactive)
  (if (local-variable-p 'mode-line-format)
      (kill-local-variable 'mode-line-format)))

Landing page

The file specified on the command line argument should be the first to be presented to the user, of course. But, as it is often the case, Emacs is launched without arguments and the startup buffer is usually *scratch*, which is rarely what I need. I’d rather start with some *Untitled* text file.

(setq initial-buffer-choice
  (lambda ()
    (if (buffer-file-name)   ;; if an actula file is already open,
        (current-buffer)     ;; leave it as it is.
      (new-untitled-file))))


(defun new-untitled-file ()
  "Create/Return an anonymous text buffer"
  (let ((buffer)
        (default-name "*Untitled*"))
    (or (get-buffer default-name)
        (with-current-buffer (get-buffer-create default-name)
          (text-mode)
          (current-buffer)))))

(defun open-untitled-file ()
  "Create/Select an anonymous text buffer"
  (interactive)
  (switch-to-buffer (new-untitled-file)))

(global-set-key (kbd "C-n") #'open-untitled-file)

Usability

Emacs definitely can improve in term of usability. Here we setup few packages in order to make it more polished and nice to use.

Minubuffer completion

The completion framework used to be based on Helm but now I am moving toward the vertico, marginalia, orderless, consult, embark mechanism. It integrates better with Emacs and does not require a specific implementation for every use case. Let’s see how it goes. Package vertico provides a basic vertical choice in the minibuffer, with filtering and completion, while marginalia shows documentation on the side of the candidate. The stuff typed at the minibuffer is used to complete/filter the choices, and orderless allows some kind of fuzzy matching.

;; Ignore case in completion.
(setq read-file-name-completion-ignore-case t
      read-buffer-completion-ignore-case t
      completion-ignore-case t
      enable-recursive-minibuffers t)


(use-package orderless
  :custom
  (completion-styles '(orderless basic))      ; Use orderless
  (completion-category-defaults nil)          ; I want to be in control!
  (completion-category-overrides
   '((file (styles basic               ; For `tramp' hostname completion with `vertico'
                   orderless)))))

(use-package vertico
  :ensure t
  :custom 
  (vertico-count 10)    ; Number of candidates to display
  (vertico-resize nil)
  :init
  (require 'orderless)  ; should be loaded before
  :config
  ;; (vertico-reverse-mode) ;; if I want prompt on the bottom line
  (require 'vertico-directory)
  (vertico-mode    1)
  (marginalia-mode 1))

(use-package marginalia
  :commands (marginalia-mode)
  :custom
  (marginalia-max-relative-age 0)
  (marginalia-align 'left))

(use-package embark
  :commands (embark-act embark-dwim)
  :bind (:map vertico-map
              ("M-q" . embark-act)
         :map embark-general-map
              ("M-q" . abort-recursive-edit)
              ("M-SPC" . abort-recursive-edit)
              )
  :custom
  ;; Show keys at the bottom (and above the minibuffer)
  (embark-indicators '(embark-verbose-indicator embark-highlight-indicator embark-isearch-highlight-indicator))
  (embark-verbose-indicator-display-action '(display-buffer-at-bottom)))

Minibuffer tool: Find File / Switch buffer / Grep

Package consult allows to have different categories of completions. It also shows file preview but honestly I don’t like that. It builds on top of vertico and has a lot of tools like consult-ripgrep.

I use it to search in files and to switch buffer. I load an extra package to add all files in the current project to buffer switcher.

(use-package consult
  :bind (("C-M-s"   . consult-ripgrep)
         ("C-x C-b" . consult-buffer)
         ("M-SPC"   . consult-buffer)
         ("C-f"     . mxl/consult-find-file)
         ("M-y"     . consult-yank-from-kill-ring)
         :map vertico-map
         ("M-SPC" . abort-recursive-edit))
  :custom
  (consult-preview-key 'nil) ; no preview
  :config
  (defun mxl/consult-find-file ()
    (interactive)
    (consult-find "~/"))
  
  (consult-customize
      consult-goto-line
      consult-line
      :preview-key ' any)  ;; immediate preview just for these tools
  )


(use-package consult-project-extra
  :after consult
  :config

  (defvar consult-project-el-files
    `(:name      "Project Files (via project.el)"
                 :category  file
                 :face      consult-file
                 :history   file-name-history
                 :action    ,#'consult-project-extra--find-with-concat-root
                 :enabled   ,#'project-current
                 :items
                 ,(lambda () (consult-project-extra--project-files (project-root (project-current))))))
  (add-to-list 'consult-buffer-sources 'consult-project-el-files 'append))

Additional action with embark, as it was in helm

I find convenient to have the choice of minibuffer action displayed in the minibuffer as helm does. Apparently this recipe works.

(defun embark-act-with-completing-read (&optional arg)
  (interactive "P")
  (let* ((embark-prompter 'embark-completing-read-prompter)
         (embark-indicators '(embark-minimal-indicator)))
    (embark-act arg)))

Prose and Technical Writing

I use Emacs to write technical papers about math, code documentation, lecture notes for my courses , blog posts, and to edit my websites… and sometimes to prepare slides. Therefore I need to setup a proper environment. I often see many emacs user writing LaTeX with for a tool which is barely setup for writing code, and definitely not right for writing prose.

Emacs has a lot of potential in prose writing, especially if you ditch LaTeX and write in Org Mode or Markdown. In this regard I suggest the following reading.

Syntax and Grammar Check

Too few people on Emacs have a decent setup for syntax checking, and even fewer have a decent setup for grammar checking. I don’t claim that my setup is especially clever, but at least it includes spell and grammar check in

  • Italian;
  • American English;
  • British English.

In particular I often write papers with colleagues who prefer the British spelling rather than American one (which I favor), therefore I keep them both.

The entry points of my setup are three functionalities

  • Syntax/Grammar check, activated with mxl/language-check
  • Switch between languages with mxl/language-switch
  • Fix interactively the typos
(global-set-key [f2]  'mxl/language-check)
(global-set-key (kbd "M-<f2>") 'mxl/language-switch)
(global-set-key (kbd "M-s") 'flyspell-correct-at-point)

The setup revolves on few packages.

Flyspell requires a working setup of Ispell. I setup the latter to make use of Hunspell, which is the default spellchecker of Libre/Openoffice and Firefox. Notice that I usually need to place my dictionaries for hunspell in a non standard directory. Hunspell look them in the directories in the DICPATH env variable. Be careful to have dictionaries for all three languages, otherwise the setup will fail. Here’s I set up the path.

;; I have hunspell dictionaries installed in my personal folder.
(let ((dicpath-pers (concat (getenv "HOME")
                            "/personal/dictionaries/hunspell")))
  (when (file-directory-p dicpath-pers)
    (setenv "DICPATH" dicpath-pers)))

Flyspell highlights typos and strikes out words that are repeated within a certain distance flyspell-duplicate-distance, which is set to 0 because I only want to signal adjacent repetitions. Notice that I activate flyspell using the first of my preferred languages, and that I use flyspell-prog-mode for programming. Flyspell allows a more interactive interface for fixing typos, contrary to the default ispell-word.

(use-package flyspell
  :commands (flyspell-mode flyspell-prog-mode flyspell-correct-at-point)
  :bind (:map flyspell-mode-map ("C-;" . nil))
  :hook ((prog-mode . flyspell-prog-mode)
         (text-mode . flyspell-mode))
  :custom
  (flyspell-duplicate-distance 0)     ;; signal as repetitions only adjacent pairs
  (flyspell-highlight-flag t)         ;; mark mispelled words
  (flyspell-issue-message-flag nil)   ;; silent checking
  (flyspell-persistent-highlight nil) ;; only highlight one word
  (flyspell-use-meta-tab nil)         ;; disable M-<tab>
  :custom-face
  (flyspell-duplicate ((t (:underline (:color "magenta" :style wave)))))
  (flyspell-incorrect ((t (:underline (:color "magenta" :style wave)))))
  :init
  (setq ispell-program-name (executable-find "hunspell"))
  (setq ispell-dictionary "english")  ;; last language by default
  (if (not ispell-program-name)
      (message "Spell checking disabled: impossible to find correctly installed 'Hunspell'."))
  :config 
  (require 'flyspell-correct))        ;; the command I bind to M-s

This is the code for chosing the preferred languages.

(defun mxl/language-switch ()
  "Switch between spell checking languages, in the current buffer."
  (interactive)
  (let* ((lang (completing-read "Pick the language: "
                                    '("english" "british" "italiano"))))
    (ispell-change-dictionary lang)
    (setq langtool-default-language
          (cond
           ((string-equal lang "italiano") "it")
           ((string-equal lang "english")  "en")
           ((string-equal lang "british")  "en-GB")
           (t "")))
    ))

Grammar check with Langtool is reasonably easy to setup. The only caveat is that it need to be installed. When installed, we use langtool-disabled-rules to deactivate some checks (e.g. whitespaces) which generates too many false positives.

(setq langtool-path '("/usr/local/share/languagetool";
			"~/.local/share/LanguageTool"
                        "/snap/languagetool/current/usr/bin"))
(setq langtool-tmp "")
(setq langtool-language-tool-jar (concat (dolist (elt langtool-path langtool-tmp)
					     (setq elt (expand-file-name elt))
					     (if (file-directory-p elt) (setq langtool-tmp elt)))
					     "/languagetool-commandline.jar"))
(when (file-exists-p langtool-language-tool-jar)
  (use-package langtool
	:config
	(setq langtool-mother-tongue "it")'
	(setq langtool-disabled-rules "WHITESPACE_RULE")
	:commands (langtool-check langtool-check-buffer langtool-switch-default-language)))

The last component is a single function mxl/language-check. Allows to

  • pick between syntax and grammar check in the current language;
  • stop grammar check if one is running;
;; Spell/Grammar check command
(defun mxl/language-check ()
  "Launch either spell check or grammar check

Offer a choice between spell checking the buffer, or grammar
checking it. It a region is active the spell check will be
performed on that region. If some grammar checking session is
open, the command will just close it.
"
  (interactive)
   ;; If grammar check is active, close it
  (if (and (boundp 'langtool-mode-line-message)
           langtool-mode-line-message)
      (langtool-check-done)
    ;; otherwise offer a choice
    (let* ((choices '("spelling" "grammar" "none"))
           (selection (ido-completing-read "Check for " choices )))
      (pcase selection
        ("spelling"
         (if (region-active-p)
             (call-interactively 'ispell-region)
           (ispell-buffer)))
        ("grammar" (langtool-check-buffer))
        (otherwise nil))
      )))

Looking stuff up from dictionaries

I have few dictionaries of various quality set to be used with sdcv command line utility. There is an emacs package for it but it is quite inconvenient to use. Therefore I just extracted from the dictionaries a wordlist that I use do write a little helper for dictionary search, set to a key.

(defvar sdcv-word-cache nil
  "Caches all words from the wordfile.")

(defvar sdcv-word-file "~/personal/dictionaries/wordlists/english_and_italian.txt"
  "File containing all words to be looked up.")

(use-package sdcv
  :commands (sdcv-search-completion)
  :bind ("C-c d" . sdcv-search-completion)
  :init
  (defun sdcv-search-completion (&optional word)
    (interactive)
    (unless sdcv-word-cache
      (setq sdcv-word-cache
            (with-temp-buffer
              (insert-file sdcv-word-file)
              (split-string (buffer-string) "\n"))))
    (sdcv-search-input (or word
                           (completing-read "Lookup in dictionary: " sdcv-word-cache nil t (word-at-point))
                           ))))

Text files, word wrapping, etc

I like when files are, to some extent, of limited width. Unfortunately it is not always possible for technical reasons. There are many options to wrap a text a specific width, either hard coding newlines, or wrapping on the display without modifying the file.

  • auto-fill-mode (hard insert line breaks automatically)
  • visual-fill-mode (visual wrap at window margin)
  • visual-fill-column-mode (visual wrap at fixed margin)

Some behavior of text filling can be controlled.

(setq sentence-end-double-space nil)   ;; no double space after period.

(add-hook 'fill-nobreak-predicate 'fill-single-char-nobreak-p) ;; never break at single char words 
(add-hook 'fill-nobreak-predicate 'fill-single-word-nobreak-p) ;; never leave a single word on a row

I am on the fence about auto-fill-mode. In general I don’t like text to automatically change while I am writing. I’d rather reformat the text willingly.

;; Fill/unfill paragraph
(use-package unfill
  :bind (:map text-mode-map ( "M-q" . unfill-toggle)))

To me, prose text files should have at most 70 columns. And when word wrapping is hard-coded, the text should be justified. This makes sense at least on pure text files, i.e., ones that are not meant to be interpreted as code or special markup.

(defun mxl/text-mode-setup ()
  "Setup for text-mode files

This applies to pure text mode files and not to modes derived
from `text-mode'."
  (interactive)
  (when (eq 'text-mode major-mode)
    (setq fill-column 70)
    (setq default-justification 'full)))

(add-hook 'text-mode-hook 'mxl/text-mode-setup)

For text files like org-mode files it is better to enable auto-fill-mode. For email messages it is better to use something like visual-fill-column-mode. The standard visual-line-mode matches the text width with the window width, which is annoying on large horizontal screen.

Notice that it is possible for visual-fill-column-mode to center the text setting visual-fill-column-center-text to t.

(use-package visual-fill-column
  :disabled
  :init
  (setq-local fill-column 70)
  (setq visual-fill-column-center-text nil)
  :config
  (advice-add 'text-scale-adjust :after #'visual-fill-column-adjust)
  :hook ((visual-line-mode . turn-on-visual-fill-column-mode)
         (visual-line-mode . turn-off-auto-fill)
         (message-mode . visual-line-mode)))

Writing prose is easier in a no-distraction setup. What did Virginia Woolf say? “A Room of One’s Own”.

(use-package writeroom-mode
  :init
  (setq writeroom-bottom-divider-width 0))

Math and Latex

Math mode is LaTeX/Org-mode

My main TeX/LaTeX setup is based on its own file, but there is a nice utility which allows to conveniently type the depecated dollas signs. By typing a dollar, the inline pair \( \) is inserted. It toogles between pair \( \) and \[ \] everytime the dollar is typed while at the end of a pair or inside an empty pair.

Recall you can type $ by preceding it with C-q.

(use-package math-delimiters
  :commands (math-delimiters-insert)
  :ensure nil)

(with-eval-after-load 'org
  (define-key org-mode-map "$" #'math-delimiters-insert))

(with-eval-after-load 'tex              ; for AUCTeX
  (define-key TeX-mode-map "$" #'math-delimiters-insert))

(with-eval-after-load 'tex-mode         ; for the built-in TeX/LaTeX modes
  (define-key tex-mode-map "$" #'math-delimiters-insert))

Bibliography

I have quite a setup in init-bibliography, but I am trying to move it to this file. A key passage is to go from helm-bibtex to citar. First let’s tell citar where is the bibtex database and the files.

We also setup some default action to use with embark. My selection of actions does not inherit the default embark actions and therefore is more focused.

(use-package citar
   :bind ("C-c b" . citar-open)
   :commands (citar-open citar-open-files citar-open-links)
   :init
   (setq citar-library-paths '("~/cloud/Papers/")) ;; used outside citar
   (setq citar-notes-paths   '("~/cloud/Papers/")) ;; notes next to papers
   :custom
   ;; setup for citar-inset-citation
   (citar-latex-default-cite-command "cite")
   (citar-latex-prompt-for-cite-style nil)
   (citar-latex-prompt-for-extra-arguments nil)
   ;; main bibliography
   (citar-bibliography '("~/lavori/latex/bibliografia.bib"))
   ;; find pdf and org notes for papers
   (citar-file-variable "file") 
   :config
   (citar-embark-mode))

;; Actions available 
(use-package citar-embark
  :after (embark citar)
  :config
  (setq citar-embark-map
        (let ((map (make-sparse-keymap)))
          (define-key map (kbd "RET") #'citar-run-default-action)
          (define-key map (kbd "c") #'citar-insert-citation)
          (define-key map (kbd "f") #'citar-open-files)
          (define-key map (kbd "l") #'citar-open-links)
          (define-key map (kbd "e") #'citar-open-entry)
          (define-key map (kbd "n") #'citar-open-notes)
          (define-key map (kbd "o") #'citar-open)
          (define-key map (kbd "r") #'citar-insert-reference)
          (define-key map (kbd "M-q") #'abort-recursive-edit)
          map)))

Appearance of the bibliography selector

Setup the look and icons for the bibliography selection. We use nerd-icons icons.

(defvar citar-indicator-files-icons
  (citar-indicator-create
   :symbol (nerd-icons-faicon
            "nf-fa-file_o"
            :face 'nerd-icons-green
            :v-adjust -0.1)
   :function #'citar-has-files
   :padding " " ; need this because the default padding is too low for these icons
   :tag "has:files"))

(defvar citar-indicator-links-icons
  (citar-indicator-create
   :symbol (nerd-icons-faicon
            "nf-fa-link"
            :face 'nerd-icons-orange
            :v-adjust 0.01)
   :function #'citar-has-links
   :padding " "
   :tag "has:links"))

(defvar citar-indicator-notes-icons
  (citar-indicator-create
   :symbol (nerd-icons-faicon
            "nf-fa-file_text"
            :face 'nerd-icons-blue
            :v-adjust -0.1)
   :function #'citar-has-notes
   :padding " "
   :tag "has:notes"))


(setq citar-indicators
  (list citar-indicator-files-icons
        citar-indicator-links-icons
        citar-indicator-notes-icons
        ))

Email drafts

The fight to have email working inside emacs is a stink. I tried notmuch and mu4e but they all fail to work well with gmail in, mostly because the IMAP interface that gmail exposes is insufficient to implement many good features of GMAIL. For example, notmuch tags are not synced between machine and that’s a serious issues.

In the end I just want something to quickly go and write email drafts. This function setup a buffer in textmode, fills it with some template and info mostly useful for bookkeeping. In the end I cut and paste the buffer into gmail client.

(defun mxl/create-mail-draft ()
  "Create a mail draft buffer in text-mode

Drafts are stored in hardcoded path '~/personal/maildrafts/'
with timestamped filename.
"
  (interactive)
  (let ((buffer)
        (spos)
        (loc "~/personal/maildrafts/")
        (name (format-time-string "%Y%m%d-%H%M%S-maildraft.txt" (current-time))))
    (switch-to-buffer (find-file-noselect (concat loc name) nil nil nil))
    (setq fill-column 70)
    (text-mode)
    (auto-fill-mode)
    (setq default-justification 'left)
    (insert "Date: " (format-time-string "%Y/%m/%d at %H:%M:%S" (current-time)) "\n")
    (insert "From: " user-full-name " <" user-mail-address ">")
    (insert "\n" (make-string fill-column ?—) "\n")
    (setq spos (point))
    (insert "\n\n\n-- \n")
    (insert (or (and (boundp 'user-mail-signature) user-mail-signature) ""))
    (goto-char spos)
    (save-buffer)))

Email via Thunderbird

Using some extension is it possible to write email using emacsclient inside thunderbird. My setup uses the custom editor choice which is setup as

emacsclient -c -F '((name . "floatemacs") (width . 100) (height . 40))' "/path/to/eml"

A new frame for emacsclient opens up. The frame has instance floatemacs, so that my i3 configuration makes it floating and puts it at the center of the screen. (See my i3 configuration file)

Thunderbird uses eml files for email, hence I need to set them up

(add-to-list 'auto-mode-alist '("\\.eml\\'" . message-mode))

Programming

Tab vs Space

Of course I use 4 spaces for indentation

(setq-default indent-tabs-mode nil) ;; Expand tabs as spaces
(setq-default tab-width 4)
(setq default-tab-width 4)

Trailing whitespaces

Removal of trailing whitespace is useful in code. I would do it in text files as well, but working in parallel on LaTeX files makes such removals problematic with co-authors. It makes merges more difficult.

But in code this is not usually an issue. So let’s set automatic deletion of trailing whitespace, but just in programming buffers.

(defun mxl/delete-trailing-whitespace-on-save-on ()
  (interactive)
  (add-hook 'before-save-hook 'delete-trailing-whitespace 0 t))

(defun mxl/delete-trailing-whitespace-on-save-off ()
  (interactive)
  (remove-hook 'before-save-hook #'delete-trailing-whitespace t))

(add-hook 'prog-mode-hook #'mxl/delete-trailing-whitespace-on-save-on)

Generic Syntax highlighting

Of course every mode include syntax highlighting for the corresponding type of file. There are more “semantic” ways to highlight pieces of codes. Many of these syntax highlight packages are described in

The first package color parenthesis with dirrerent colors, so that matching parenthesis have the same color. Of course after some nesting the colors repeat. This is especially useful for lisp programming.

(use-package rainbow-delimiters
  :diminish ""
  :init (setq rainbow-delimiters-max-face-count 4)
  :commands rainbow-delimiters-mode
  :hook ((emacs-lisp-mode . rainbow-delimiters-mode)
         (lisp-interaction-mode . rainbow-delimiters-mode)))

The next package colors each identifier, so that the occurrences of that identifier have the same color than the definition.

(use-package rainbow-identifiers
  :diminish ""
  :commands rainbow-identifiers-mode)

Next package sets a different background color for each nested code block.

(use-package highlight-blocks
  :diminish ""
  :commands highlight-blocks-mode)

Next package highlights occurrences of the symbol under the point.

(use-package highlight-symbol
  :diminish ""
  :commands highlight-symbol-mode)

Next package highlight lisp quotes, so it is not useful for other languages (maybe Scheme?).

(use-package highlight-quoted
  :diminish ""
  :commands highlight-quoted-mode)

Next package is useful for emacs lisp. It highlight names that are defined in emacs lisp, differentiating functions, variables, builtins, …

(use-package  highlight-defined  :ensure t
  :commands highlight-defined-mode)

Highlight notes in comment

This code was stolen by Casey Muratori aka Molly Rocket and highlight some words in code to make annotations. I implement

  • TODO (Red)
  • NOTE (Green)
  • CITE (Blue)
  • ALERT (Yellow)
  • WARNING (YELLOW)
(setq mxl/annotation-prog-modes
      '(python-mode
        c  -mode
        c-mode
        go-mode
        rust-mode
        sh-mode
        latex-mode
        LaTeX-mode
        emacs-lisp-mode))

(make-face 'font-lock-fixme-face)
(make-face 'font-lock-note-face)
(make-face 'font-lock-cite-face)
(make-face 'font-lock-alert-face)
(mapc (lambda (mode)
        (font-lock-add-keywords
         mode
         '(("\\<\\(TODO\\)" 1 'font-lock-fixme-face t)
	       ("\\<\\(NOTE\\)" 1 'font-lock-note-face t)
	       ("\\<\\(ALERT\\)" 1 'font-lock-alert-face t)
	       ("\\<\\(WARNING\\)" 1 'font-lock-alert-face t)
	       ("\\<\\(CITE\\)" 1 'font-lock-cite-face t))))
	  mxl/annotation-prog-modes)

(modify-face 'font-lock-fixme-face "Red" nil nil t nil nil nil nil)
(modify-face 'font-lock-note-face "Light Green" nil nil t nil nil nil nil)
(modify-face 'font-lock-cite-face "Light Blue" nil nil t nil nil nil nil)
(modify-face 'font-lock-alert-face "Yellow" nil nil t nil nil nil nil)

Compiling and building

The compile, project-compile and recompile commands are useful starting points. I run compilation commands using compile-multi that allows me to setup nice build commands. The compilation buffer does not like ansi color sequences by default. We fix that.

(use-package compile
  :config
  (add-hook 'compilation-filter-hook 'ansi-color-compilation-filter)
  ;; error regexp for pyright
  (add-to-list 'compilation-error-regexp-alist-alist
               '(pyright "^[[:blank:]] \\(. \\):\\([0-9] \\):\\([0-9] \\).*$" 1 2 3))
  (add-to-list 'compilation-error-regexp-alist 'pyright))

Project.el, used just to find project roots

We do the least possible setup for project. A remark: I prefer to use build using the innermost Makefile from the current directory, in case there are many. This is not necessarily a universal good thing but works for some of me.

(use-package project
  :init
  (setq project-vc-merge-submodules nil)
  :config
  (add-to-list 'project-vc-extra-root-markers "Makefile")
  (add-to-list 'project-vc-extra-root-markers ".latexmkrc"))

Compile-multi is the build commands entry point

With that I can do the basic setup of my compilation process.

(setq compile-command nil)             ;; no default command
(setq compilation-ask-about-save nil)
(setq compilation-read-command t)      ;; ask the command when using
                                       ;; `compile' explicitly.

Now we setup compile-multi to always launch compilations from the project root. We also setup a default action that asks the compile command explicitly, and we adapt the recompile command to work with this setup.

(defun mxl/compile-default-directory ()
  "Find the directory from which to run compilation commands

Uses project.el to find project root and if none, uses the
current directory.
"
  (or (and (project-current nil)
           (project-root (project-current nil)))
      default-directory))

(use-package compile-multi
  :commands compile-multi
  :bind
  (("<f9>" .   'mxl/compile-multi-recompile)
   ("M-<f9>" . 'compile-multi))
  :init
  (setq compile-multi-config nil)
  (setq compile-multi-default-directory #'mxl/compile-default-directory)

  (defun mxl/compile-multi-recompile ()
    (interactive)
    (if (and compile-command
             (equal compilation-directory
                    (funcall compile-multi-default-directory)))
        (recompile)
      (compile-multi)))

  :config
  (push `(t
          ("Fallback:Edit command" . ,#'(lambda ()
                                          (call-interactively 'compile))))
        compile-multi-config))

Build LaTeX documents

Here we have an example on how to add compile actions for LaTeX. If the project for a latex document has a .latexmkrc file, then we can use latexmk to produce that document. These actions won’t be available in projects which do not contain .latexmkrc.

(push `(latex-mode
        ("LaTeX:Run pdflatex" "pdflatex"
         TeX-command-extra-options
         "-file-line-error"
         "-interaction=nonstopmode"
         "--synctex=1"
         (concat (TeX-master-file) ".tex"))
        ("LaTeX:Run xelatex" "xelatex"
         TeX-command-extra-options
         "-file-line-error"
         "-interaction=nonstopmode"
         "--synctex=1"
         (concat (TeX-master-file) ".tex"))
        ("LaTeX:Run bibtex" "bibtex"
         (concat (TeX-master-file)))
        ("LaTeX:Lint with LaCheck" "lacheck" (concat (TeX-master-file) ".tex"))
        ("LaTeX:Lint with ChkTeX"  "chktex -v6"  (concat (TeX-master-file) ".tex"))
        ("LaTeX:AUCTex Command" . ,#'TeX-command-master))
        compile-multi-config)

(push `((file-exists-p ".latexmkrc")
        ("LatexMK:compile document" . "latexmk")
        ("LatexMK:clean" . "latexmk -C")
        ("LatexMK:view externally"  . "latexmk -pv"))
        compile-multi-config)

Export Org documents to LaTeX

To “build” org files there is the org-export-dispatch command. I could bind the command directly to F9 but I want to experiment the integration with compile-multi. Maybe it will just be one more key to press most of the time, but it is useful when the org file is inside some project that has more build options. Those options would be unavailable when org-export-dispatch is bound to F9. Nevertheless I use org export mostly to build PDFs via latex and beamer, therefore I setup direct actions for those as well.

(push `(org-mode
        ("Org:PDF via latex"   . ,#'(lambda () (org-open-file (org-latex-export-to-pdf nil))))
        ("Org:PDF via beamer"  . ,#'(lambda () (org-open-file (org-beamer-export-to-pdf nil))))
        ("Org:export (choose)" . ,#'org-export-dispatch)
        ("Org:export (last)"   . ,#'(lambda () (org-export-dispatch t))))
        compile-multi-config)

Building using Make

Useful compile/build commands are the ones built into makefiles. I have asked chat-gpt to write me a function that parses Makefiles (I don’t know much emacs-lisp). This example was inspired by https://github.com/mohkale/compile-multi tutorial.

(defun mxl/parse-makefile-for-targets (makefile)
  "Parse the MAKEFILE and return a list of valid build targets.

Written by chat-gpt"
  (interactive "fMakefile: ")
  (let ((targets '()))
    (with-temp-buffer
      (insert-file-contents makefile)
      (goto-char (point-min))
      (while (re-search-forward "^\\([a-zA-Z0-9_\\-] \\)\\s-*:[^=]" nil t)
        (let ((target (match-string 1)))
          ;; Ensure it's not a special target or a variable
          (unless (string-match-p "^\\." target)
            (push target targets))))
      (reverse targets))))

(defun mxl/extract-compile-rules-from-makefile ()
    (mapcar 
     #'(lambda (target) (cons (concat "Make:" target) (concat "make " target)))
     (mxl/parse-makefile-for-targets "Makefile")))

(push `((file-exists-p "Makefile")
        ("Make:make" . "make")
        ,#'mxl/extract-compile-rules-from-makefile)
      compile-multi-config)

Building via shell scripts

Sometimes the simplest and most convenient way to build a project is just to run a shell script. This setup makes it possible to run the shell scripts that are in the root folder.

(defun mxl/extract-compile-shell ()
    (mapcar 
     #'(lambda (target) (cons (concat "Shell script:" target) target))
     (seq-filter 'file-regular-p (directory-files "./" nil "\\.sh$"))))

(push `(t
        ,#'mxl/extract-compile-shell)
      compile-multi-config)

Fixing Errors

We all know most of development time is spend looking for error and fixing them. Here we up set the basic function that Emacs has to compile and navigate the errors previous-error and next-error. The function keys from F9 to F12 are reserved for development needs, and the functionality strongly depends on the current mode. In particular I would like

  • F9 compile/recompile;
  • F10 debug/interact;
  • F11 previous error;
  • F12 next error.
(global-set-key [f11] 'previous-error)
(global-set-key [f12] 'next-error)

Behavior of the compilation windows

My default behavior is that the compilation window stays visible if there are errors, but will disappear if there are no errors reported by the compilation process. It should be noticed that unfortunately some lint tools exit with zero even in presence of warnings or errors.

(defvar mxl/compilation-window-stays-visible nil
  "Whether the compilation windows should stay visible after
a successful compilation.")

(defun mxl/compilation-exit-autoclose (status code msg)
  ;; If M-x compile exists with a 0
  (if (and (eq status 'exit)
           (zerop code)  ;; check errors
           (not mxl/compilation-window-stays-visible)) 
      (progn ;; bury and delete compilation windows
        (let ((cmpl-buffername (buffer-name)))
          (bury-buffer cmpl-buffername)
          (delete-window (get-buffer-window (get-buffer cmpl-buffername)))))
    (setq mxl/compilation-window-stays-visible t))
  ;; Always return the anticipated result of compilation-exit-message-function
  (cons msg code))

;; Specify my function (maybe I should have done a lambda function)
(setq compilation-exit-message-function 'mxl/compilation-exit-autoclose)

Sometimes I want to see the compilation window anyway, and for that I use M-F11 and M-F12 to show it and make it stay visible across compilations.

(defun mxl/toggle-compilation-window ()
  "Show/Hide the window containing the '*compilation*' buffer."
  (interactive)
  (when-let ((buffer next-error-last-buffer))
    (if (get-buffer-window buffer 'visible)
        (progn
          (setq mxl/compilation-window-stays-visible nil)
          (delete-windows-on buffer))
      (display-buffer buffer)
      (setq mxl/compilation-window-stays-visible t))))

(global-set-key (kbd "M-<f11>") #'mxl/toggle-compilation-window)
(global-set-key (kbd "M-<f12>") #'mxl/toggle-compilation-window)

Inferior shell

I like the inferior shell to scroll to bottom when I send code to be executed.

(setq comint-scroll-to-bottom-on-input t)
(setq comint-scroll-to-bottom-on-output t)
(setq comint-move-point-for-output t)

Programming language support (LSP)

The most convenient way to integrate language features nowadays is to use a language server that implements LSP (Language Server Protocol). I am currenly using Eglot which should provide, and set up on its own, several features as

  • completion using corfu
  • integrated help using eldoc
  • jump to/from definitions using xref (try M-, M-. keybindings)
  • code snippets (if yasnippet is enabled in advance)
  • code checking using flymake (that I disable)
  • other features…
(defun mxl/eglot-format-region-or-defun ()
  (interactive)
  (save-mark-and-excursion
    (if (not (region-active-p))
        (mark-defun))
    (call-interactively 'eglot-format)))

(use-package eglot
  :config
  (add-to-list 'eglot-stay-out-of 'flymake)
  (diminish 'eldoc-mode))

We configure basic keybindings for the modes that use eglot

Python

Python is the programming language I am using the most, nowadays. My setup is not very sophisticated right now. Earlier it was more complex but it was breaking quite often at each change of technology, Ipython, and so on…

My configuration is based around the default python.el included with Emacs (there are indeed other python modes around). Furthermore it uses

  • eglot as a LSP client, providing help, completion, symbol search;
  • requires to run pip install pyright to install a decent language server;
  • pyenv to manage the virtual environments.
  • standard python as interactive shell (Ipython prompt and completion tend to confuse Emacs if not well configured).
  • yapfify to format code.

Binaries will be found in the pyenv paths.

(add-to-list 'exec-path (concat (getenv "HOME") "/.pyenv/shims"))
(add-to-list 'exec-path (concat (getenv "HOME") "/.pyenv/bin"))
(environment-add-path (concat (getenv "HOME") "/.pyenv/shims"))
(environment-add-path (concat (getenv "HOME") "/.pyenv/bin"))

Setup of compile commands for python. In particular syntax checks via Flake8 or pylint, and pytest commands.

(push `(python-mode
        ;; pytest
        ("Python:pytest (all tests)"         . "pytest")
        ("Python:pytest (last failed)"       . "pytest --lf")
        ("Python:pytest (last failed first)" . "pytest --ff")
        ("Python:pytest (single file)" "pytest --doctest-modules" (file-relative-name buffer-file-name))
        ;; Syntax/Lint checks
        ("Python:basic syntax check"
         "python" "-m" "py_compile"  (file-relative-name buffer-file-name))
        ("Python:Lint with pylint"
         "pylint"                    (file-relative-name buffer-file-name))
        ("Python:Lint with flake8"
         "flake8"                    (file-relative-name buffer-file-name))
        ("Python:Lint with ruff"
         "ruff check"                (file-relative-name buffer-file-name))
        ("Python:Lint with pyright"
         "pyright"                    (file-relative-name buffer-file-name)))
        compile-multi-config)

Using the key F10 I can toggle the prompt shell buffer connected to this python buffer.

By default C-c C-c uses python-shell-send-buffer which does not execute the content of main by default. I usually need it, therefore I remap the key.

To format code I use yapfify. I use this custom function to either format regions or function definitions.

(defun mxl/yapfify-region-or-defun ()
  (interactive)
  (if (python-syntax-comment-or-string-p)
      (python-fill-paragraph t)
    (save-mark-and-excursion
      (if (not (region-active-p))
          (python-mark-defun))
      (call-interactively 'yapfify-region))))
(use-package python
  :init
  (setq python-indent-guess-indent-offset nil
        python-indent-offset 4)
  :hook ((python-mode . eglot-ensure))
  :bind (:map python-mode-map
              ("M-q"     .  mxl/yapfify-region-or-defun)
              ("C-M-q"   .  yapfify-buffer)
              ("<f10>"   .  python-shell-switch-to-shell)
              ("M-<f10>" .  run-python)
              ;; Force the execution of the main block
              ("C-c C-c" .  (lambda () (interactive) (python-shell-send-buffer t)))
         :map inferior-python-mode-map
              ("<f10>" . delete-window)))

Now I use Eglot which provides eglot-format with calls Yapf to enforce code formatting. I would like to run it automatically before saving.

Emacs is able to find the appropriate pyenv Python Environment and to load it automatically. Which is useful for running syntax checkers or the Python Shell. Notice that by default pyenv-mode sets key bidings on C-c C-s and C-c C-u, which conflicts with tons of stuff, including org-schedule. Hence I disable such bindings, since pyenv-mode-auto sets the python environment automatically according .python-version. In the rare cases where I need to switch environment I usually run the command from the minibuffer.

(use-package pyenv-mode
  :bind (:map pyenv-mode-map
         ("C-c C-s" . nil)
         ("C-c C-u" . nil))
  :commands (pyenv-mode-set pyenv-mode-unset pyenv-mode))

(use-package pyenv-mode-auto
 :hook (find-file . pyenv-mode-auto-hook)
 :config (pyenv-mode))

C and C

Setup C and C modes, using Eglot which in turn uses ccls by default.

(defun mxl/cc-mode-setup ()
  "Setup for C/C   modes"
  (set-fill-column 78))

(use-package cc-mode
  :config
  (setq c-basic-offset 4)
  (setq c-block-comment-prefix "")
  :bind (:map c-mode-map
         ("RET"     .  newline-and-indent)
         :map c  -mode-map
         ("RET"     .  newline-and-indent))
  :hook ((c-mode    . eglot-ensure)
         (c  -mode  . eglot-ensure)
         (c-mode    . mxl/cc-mode-setup)
         (c  -mode  . mxl/cc-mode-setup)))

One annoyance I had when I started using more complex programming environments than Turbo C/C was that, sure we need a project file, or a Makefile, and so on… but sometimes I just want to write a single program and compile it and run it.

(push `(c-mode
        ("C programming:compile"
         "gcc"
         (file-relative-name buffer-file-name)
         "-Wall -Werror" "-g"  "-o"
         (file-name-sans-extension (file-relative-name buffer-file-name)))
        ("C programming:compile and run"
         "gcc"
         (file-relative-name buffer-file-name)
         "-Wall -Werror" "-g"  "-o"
         (file-name-sans-extension (file-relative-name buffer-file-name))
         "&&"
         (concat "./" (file-name-sans-extension (file-relative-name buffer-file-name)))))
        compile-multi-config)

(push `(c  -mode
        ("C   programming:compile"
         "g  "
         (file-relative-name buffer-file-name)
         "-Wall -Werror" "-g"  "-o"
         (file-name-sans-extension (file-relative-name buffer-file-name)))
        ("C   programming:compile and run"
         "g  "
         (file-relative-name buffer-file-name)
         "-Wall -Werror" "-g"  "-o"
         (file-name-sans-extension (file-relative-name buffer-file-name))
         "&&"
         (concat "./" (file-name-sans-extension (file-relative-name buffer-file-name)))))
        compile-multi-config)

Go programming

I don’t know much about go programming, but it could be useful to setup compile-multi for it.

(push `((file-exists-p "go.mod")
        ("Go project:run   "  "go run ./...")
        ("Go project:format"  "gofmt ./...")
        ("Go project:build "  "go build ./...")
        ("Go project:lint  "  "golint ./...")
        ("Go project:vet   "  "go vet ./..."))
        compile-multi-config)

(push `(go-mode
        ("Go file:run"    "go run"   (file-relative-name (buffer-file-name)))
        ("Go file:format" "gofmt"    (file-relative-name (buffer-file-name)))
        ("Go file:build"  "go build" (file-relative-name (buffer-file-name)))
        ("Go file:lint"   "golint"   (file-relative-name (buffer-file-name)))
        ("Go file:vet"    "go vet"   (file-relative-name (buffer-file-name))))
      compile-multi-config)

Emacs Lisp

It is nice to have documentation for the stuff we do and read in elisp code. So it is a good idea to turn on eldoc if possible.

(add-hook 'emacs-lisp-mode-hook 'turn-on-eldoc-mode)
(add-hook 'lisp-interaction-mode-hook 'turn-on-eldoc-mode)
(add-hook 'ielm-mode-hook 'turn-on-eldoc-mode)

Scheme

I am not a scheme programmer but just in case I want to finally read “Structure and Interpretation of Computer Programs” I have some minimal setup ready.

(use-package xscheme
  :config
  (setq scheme-program-name
        (or
         (executable-find "mit-scheme")
         (executable-find "/usr/local/bin/mit-scheme")
         (executable-find "/Applications/MITScheme.app/Contents/Resources/mit-scheme")
         "scheme"
         ))
  (if (file-name-directory scheme-program-name)
      (add-to-list 'exec-path (file-name-directory scheme-program-name) 'append))
  :commands (run-scheme scheme-mode))

Shell programming

I don’t have a particularly clever setup for shell programming, even though I should probably take it more seriously. There many bad patterns in shell programming and few good ones. I should massively use snippets. One thing that is needed is to activate shell mode automatically for Zsh scripts.

I don’t know muc shell programming. Maybe I should make templates for good bash programming practices as in

(add-to-list 'auto-mode-alist '("\\.zsh" . sh-mode))

Furthermore it is possible to edit command lines while using terminal. This setup activates sh-mode there as well

(add-to-list 'auto-mode-alist '("zsh[a-zA-Z0-9]*" . sh-mode))
(add-to-list 'auto-mode-alist '("bash-fc-[0-9]*"  . sh-mode))

There is a nice syntax checker for shell script called shellcheck, which is a godsend considering how brittle is the language. One day I will set it up.

Copyright notice and timestamps

Automatic update of the copyright notices and time stamps. Useful to keep additional track about files age, regardless of git history.

(setq copyright-query nil)                       ; do not ask before a copyright
(add-hook 'before-save-hook 'copyright-update)   ; update copyright if present
(add-hook 'before-save-hook 'time-stamp)         ; insert timestamp

Math packages

I don’t use math packages often, and I should probably use them way more. Anyway there is some support for them in Emacs, so why not having a basic setup?

Mathematica, Octave and Matlab all use files with .m extension, which is the same extension of Objective-C files. Since I do not use either much, I don’t need to fiddle with auto-mode-alist.

Matlab

There is run-octave so I guess run=matlab should exists.

(use-package matlab
  :init
  (setq matlab-shell-command
        (or
         (executable-find "matlab")
         (executable-find "/usr/local/bin/matlab")
         (executable-find "/Applications/Matlab.app/bin/matlab")))
  (setq matlab-indent-function-body t)
  :commands (matlab-mode matlab-shell))


(defalias 'run-matlab 'matlab-shell)

Mathematica

Nowadays Mathematica is a super-cool software that works on the network and has great AI. I guess it is nicer to use via the appropriate GUI or on the web. Still, emacs rocks.

(use-package wolfram-mode
  :init
  (setq wolfram-program
      (or
       (executable-find "math")
       (executable-find "/usr/local/bin/math")
       (executable-find "/Applications/Mathematica.app/Contents/MacOS/MathKernel")))
  :commands (run-wolfram wolfram-mode))

(defalias 'run-mathematica 'run-wolfram)

Reading Material

PDF Viewer

In the PDF view, I cat use g to open the outline, therefore I like to use the same key to quit it, having a toogle key.

(use-package pdf-outline
  :commands (pdf-outline)
  :bind (:map pdf-outline-buffer-mode-map
              ("g"   . quit-window)
              ("RET" . pdf-outline-display-link) ))
;; Setup PDF tools
(use-package pdf-tools
  :mode  ("\\.pdf" . pdf-view-mode)
  :magic ("%PDF" . pdf-view-mode)
  :init
  (setq pdf-view-use-unicode-ligther nil)  ;; make loading faster
  :config
  (delete 'pdf-outline-minor-mode pdf-tools-enabled-modes)
  (pdf-tools-install :no-query)
  (pdf-loader-install t)
  (add-hook 'pdf-view-mode-hook (lambda ()
                                  (auto-revert-mode 1)
                                  (beacon-mode -1)
                                  ))
  (setq pdf-view-use-scaling t)
  (setq revert-without-query '(".pdf"))
  (setq-default pdf-view-display-size 'fit-page)
  ;; Highlights do not open the annotation window when created
  (setq pdf-annot-activate-created-annotations nil)
  (defun mxl/pdf-annot-add-text-annotation ()
    "Add text annotation but forces activation which is off
by default."
    (interactive)
    (let ((pdf-annot-activate-created-annotations t))
      (call-interactively 'pdf-annot-add-text-annotation)))
  ;; more fine-grained zooming
  (setq pdf-view-resize-factor 1.1)
  ;; use isearch instead of swiper
  (define-key pdf-view-mode-map (kbd "C-s") 'isearch-forward)
  ;; keyboard shortcut for zooming
  (define-key pdf-view-mode-map (kbd " ") 'pdf-view-enlarge)
  (define-key pdf-view-mode-map (kbd "=") 'pdf-view-enlarge)
  (define-key pdf-view-mode-map (kbd "-") 'pdf-view-shrink)
  (define-key pdf-view-mode-map (kbd "0") 'pdf-view-scale-reset)
  (define-key pdf-view-mode-map (kbd "W") 'pdf-view-fit-width-to-window)
  (define-key pdf-view-mode-map (kbd "H") 'pdf-view-fit-height-to-window)
  (define-key pdf-view-mode-map (kbd "P") 'pdf-view-fit-page-to-window)
  ;; Movements
  (define-key pdf-view-mode-map (kbd "u") 'pdf-history-backward)
  (define-key pdf-view-mode-map (kbd "o") 'pdf-history-forward)
  (define-key pdf-view-mode-map (kbd "i") 'pdf-view-previous-line-or-previous-page)
  (define-key pdf-view-mode-map (kbd "k") 'pdf-view-next-line-or-next-page)
  (define-key pdf-view-mode-map (kbd "j") 'pdf-view-previous-page)
  (define-key pdf-view-mode-map (kbd "l") 'pdf-view-next-page)
  (define-key pdf-view-mode-map (kbd "M-u") 'pdf-history-backward)
  (define-key pdf-view-mode-map (kbd "M-o") 'pdf-history-forward)
  (define-key pdf-view-mode-map (kbd "M-i") 'pdf-view-previous-line-or-previous-page)
  (define-key pdf-view-mode-map (kbd "M-k") 'pdf-view-next-line-or-next-page)
  (define-key pdf-view-mode-map (kbd "M-j") 'pdf-view-previous-page)
  (define-key pdf-view-mode-map (kbd "M-l") 'pdf-view-next-page)

  ;; Links
  (define-key pdf-links-minor-mode-map (kbd "f") 'pdf-links-action-perform)
  
  ;; History
  (define-key pdf-history-minor-mode-map (kbd "B") nil)
  (define-key pdf-history-minor-mode-map (kbd "N") nil)
  (define-key pdf-history-minor-mode-map (kbd "l") nil)
  (define-key pdf-history-minor-mode-map (kbd "r") nil)
  (define-key pdf-history-minor-mode-map (kbd "u") #'pdf-history-backward)
  (define-key pdf-history-minor-mode-map (kbd "o") #'pdf-history-forward)
  (define-key pdf-history-minor-mode-map (kbd "M-u") #'pdf-history-backward)
  (define-key pdf-history-minor-mode-map (kbd "M-o") #'pdf-history-forward)

  ;; Outline
  (define-key pdf-view-mode-map (kbd "g") 'pdf-outline)

  ;; Open in external apps
  (defun mxl/pdf-open-in-xournal()
    "Open the current PDF in Xournal, for editing"
    (interactive)
    (start-process "open-pdf-in-xournal" nil "xournalpp" (buffer-file-name)))
  
  (defun mxl/pdf-open-in-default-viewer()
    "Open the current PDF in default viewer"
    (interactive)
    (let ((viewer
          (cond
           ((eq system-type 'gnu/linux) "xdg-open")
           ((eq system-type 'darwin) "open")
           (t "xdg-open"))))
      (start-process "open-pdf-in-viewer" nil viewer (buffer-file-name))))
  
  (define-key pdf-view-mode-map (kbd "X") 'mxl/pdf-open-in-xournal)
  (define-key pdf-view-mode-map (kbd "O") 'mxl/pdf-open-in-default-viewer)

  ;; Margin removal
  (defun mxl/pdf-view-toggle-crop()
    "Crop/Uncrop according to the bounding box"
    (interactive)
    (if (pdf-view-current-slice)
        (pdf-view-reset-slice)
      (pdf-view-set-slice-from-bounding-box)))
  (define-key pdf-view-mode-map (kbd "c") 'mxl/pdf-view-toggle-crop)
  ;; keyboard shortcuts for annotations
  (define-key pdf-view-mode-map (kbd "h") 'pdf-annot-add-highlight-markup-annotation)
  (define-key pdf-view-mode-map (kbd "t") 'mxl/pdf-annot-add-text-annotation)
  (define-key pdf-view-mode-map (kbd "L") 'pdf-annot-list-annotations)
  (define-key pdf-view-mode-map (kbd "D") 'pdf-annot-delete))

Books

I manage books via Calibre, which I use to store and manage my PDFs, Ebook, and so on. The package calibredb allows to access and open the books from inside Emacs.

(use-package calibredb
  :commands (calibredb)
  :init
  (setq calibredb-id-width 5)
  (setq calibredb-format-all-the-icons nil)
  (setq calibredb-format-character-icons nil)
  :config
  (setq calibredb-root-dir (expand-file-name "~/cloud/Books/"))
  (setq calibredb-db-dir (concat calibredb-root-dir "/metadata.db")))

Writing facilities

I am not very happy with this setup, but it fits with the fact that I don’t like completion and snippet expansion to step on each other toes.

In my workflow completion should only be about stuff I am typing, while templates and snippets should trigger only when explicitly asked. In my configuration templates (actually snippets) all start with characters '<' followed by some text. This is unlikely to be in the text by accident. For this reason I don’t use yasnippet-snippets because it fills by typing space with snippets which trigger out of the blue.

Completion

I have a simple setup based on corfu, which complements vertico, which fits the same role in the minibuffer. Contrary to tools like company, corfu uses the same logic and technology of vertico (e.g. orderless matching of candidates), and whatever is returned by completion-at-point. This setup is very basic but functional, and we use cape to add some extensions to it.

(use-package corfu
  :custom
  (corfu-auto t)
  (corfu-auto-delay 0.1)
  (corfu-separator ?\s) ;; M-s by default. This inserts orderless
                        ;; field separator
  (corfu-quit-no-match 'separator)
  (corfu-bar-width 0)
  (corfu-left-margin-width 0.1)
  (corfu-right-margin-width 0.1)
  (corfu-preview-current nil)
  (corfu-quit-no-match t)                   ;; i hate the "no match" warning
  :bind
  (:map corfu-map ("RET" . nil)             ;; RET does not complete
        ("M-g" . corfu-info-location)       ;; show definition
        ("M-h" . corfu-info-documentation)  ;; show help info
        ("M-o" . corfu-insert))             ;; insert completion candidate
  :init
  (global-corfu-mode)
  :config
  (corfu-popupinfo-mode))

;; Usa cape-dabbrev only with prefix length 4, and only in text modes 
(defun mxl/add-dabbrev-completion ()
  (add-to-list 'completion-at-point-functions (cape-capf-prefix-length #'cape-dabbrev 5)))

(add-hook 'text-mode-hook #'mxl/add-dabbrev-completion)

(use-package cape
  :custom
  (cape-dabbrev-check-other-buffers 'cape--buffers-major-mode) ;; only look for words in buffers with the same major modes
  (cape-dabbrev-min-length 5)
  :init
  ;; (add-to-list 'completion-at-point-functions #'cape-dabbrev)
  (add-to-list 'completion-at-point-functions #'cape-file)   ;; file paths
  (add-to-list 'completion-at-point-functions #'cape-emoji)  ;; emoji
  ;; (add-to-list 'completion-at-point-functions #'cape-tex)    ;; Unicode chars from latex commands
  )

Templates - Setup of Yasnippet.el

I use Yasnippet both for code snippets and for file template. Previously I set up Yasnippet so that it would insert a template for new files. It would not work well for LaTeX files since I need many types of templates for them, and in general whenever some tools would create new empty files. I trigger the file template explicitly.

I do not use yasnippet-snippets. I only use my own sparse collection of snippets (some copied and edited from that package anyway).

(setq mxl/local-snippet-dir (concat base-config-path "snippets/"))

Basic setup of Yasnippet.

(use-package yasnippet
  :commands (yas-minor-mode yas-global-mode yas-expand)
  :diminish (yas-minor-mode . "")

  ;; edit the snippets
  :mode  ("\\.yasnippet" . snippet-mode)
  :mode  ("\\.snippet" . snippet-mode)

  :init
  (add-to-list 'warning-suppress-types '(yasnippet backquote-change))
  (yas-global-mode 1)

  :config
  (setq yas-snippet-dirs (list mxl/local-snippet-dir))
  (setq yas-indent-line 'fixed)
  (progn
    (yas-reload-all)))

We need to be careful about whitespace, while editing snippet specs.

(add-hook 'snippet-mode-hook (lambda ()
                               (whitespace-mode)
                               (make-local-variable 'require-final-newline)
                               (setq require-final-newline nil)
                               ))

Choice and expansion of templates (menu)

There are two access point to templates/snippets.

  1. One is by expanding a template chosen from a menu. I map the menu to M-t keybind, since I’ve never used it to transpose words.
(use-package consult-yasnippet
  ;; M-t is usually reserved to transpose words, but I've never
  ;; used it.
  :bind ("M-t" . consult-yasnippet))
  1. By expanding during typing. All my snippets either do not have a key, hence are accessible only from the menu, or the key starts with '<' .

Automatic/Smart parenthesis

Emacs can help with inserting pairs of parenthesis, brackets, and so on… Even more complex pairs like \left{ and \right}. It should manage automatic wrapping of text, and stuff like that. Of couse, as usual with emacs, there are multiple functions and packages trying to do the same thing and therefore fight with each other. I use smartparens, but there are other options like autopair or electric-pair.

(use-package smartparens
  :commands (smartparens-mode smartparens-global-mode)
  :diminish ""
  :custom
  (sp-base-key-bindings nil)
  (sp-highlight-pair-overlay nil)
  (sp-wrap-repeat-last 2)
  :config
  (require 'smartparens-config)
  (sp-local-pair 'text-mode "" ""))

Other features

Backup and autosave of files

Emacs manages multiple backups for files, and furthermore it remembers to save stuff from time to time. I’d rather put all my backups and autosaves in a specific folder.

(defvar mxl/autosave-dir "~/.emacs.d/autosaves/")
(defvar mxl/backup-dir   "~/.emacs.d/backups/")
(make-directory mxl/autosave-dir t)
(make-directory mxl/backup-dir t)

First I setup the autosave feature

(setq tramp-auto-save-directory mxl/autosave-dir)
(setq auto-save-file-name-transforms
      `((".*" ,mxl/autosave-dir t)))

then I setup the backup features, where copies of old versions are kept. Disable backups for remote files.

(setq make-backup-files t)

(setq version-control nil)

(setq delete-old-versions t) ;; silently delete old versions
(setq kept-new-versions 3)   ;; number of newest versions to keep
(setq kept-old-versions 2)   ;; number of oldest versions to keep
(setq backup-by-copying t)
(setq backup-by-copying-when-linked t)

(setq backup-directory-alist
      `(("." . ,mxl/backup-dir)                ;; location for local files
        (,tramp-file-name-regexp .  nil)))     ;; disable for remote files

Bookmarks

If you are like me you have several files you visit on a regular bases, for some periods of time. E.g., lecture folders, course journal, the tex files for the paper you are currently working on. It is convenient to bookmark such locations, but the bookmark file should be in some private versioned folder.

(setq bookmark-default-file "~/personal/conf/emacs.bookmarks")

I use consult-buffer to access and set bookmarks, but it is possible to edit and delete them with command bookmark-bmenu-list, bound to C-x r l.

The list of recent files is saved on Emacs exit and kill by default. To err on safety, let’s write the file

(use-package recentf
    :demand t
    :config
    (recentf-mode 1)
    (run-at-time nil (* 60 60) 'recentf-save-list))

Make register comfortable

I set the combos M-0 M-1 up to M-0 M-9 for saving the point in the respective register, and I use the corresponding M-1M-9 to jump back to it.

(global-unset-key (kbd "M-0"))  ; I need it as a prefix key for next keybinds.

(dotimes (i 9)
  (let ((num (  1 i))
        (str (number-to-string (  1 i))))
    (global-set-key
     (kbd (concat "M-0 M-" str))
     `(lambda () (interactive) (point-to-register ,num)
        (message (concat "Position saved to register " ,str))))
    (global-set-key
     (kbd (concat "M-" str))
     `(lambda () (interactive) (jump-to-register ,num)
        (message (concat "Position loaded from register " ,str))))))

Regexp search/replace and builder

Calling replace and query-replace on regexp(s) is quite tricky. It is difficult to understand what the matched text is, especially when using the emacs regexp syntax. The visual-regexp package helps.

(use-package visual-regexp
  :commands (vr/replace vr/mc-mark vr/query-replace)
  :bind
  (("M-%" . vr/replace)
   ("C-M-%" . vr/query-replace)
  ))

This is the default setup of regexp builder.

(use-package re-builder
  :init
  (setq reb-re-syntax 'string)
  )

About

It is my Emacs configuration, which does not start with a .emacs file anymore... hail to init.el!

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published