Emacs config

Table of Contents

1 Vanilla Emacs setup

This is required to enable lexical binding in the resulting .el file, which will be used by emacs at startup.

;; -*- lexical-binding: t -*-
(setq inhibit-startup-message t)
(setq default-directory (expand-file-name "~/"))

;; Default font-family, this will used in all the function that manipulates font settings
(defvar my/font-family "Iosevka")

;; Change backups directory to emacs folder
;; this avoid to save backup files in the same directory of the original files
(setq backup-directory-alist `(("." . ,(concat user-emacs-directory
                                               "backups"))))

;; Revert dired and other buffers when there are changes on disk
(setq global-auto-revert-non-file-buffers t)

;; Hide the bell in the center of screen
(setq ring-bell-function 'ignore)
(column-number-mode t)
(global-hl-line-mode 1)

(when (member my/font-family (font-family-list))
  (set-face-attribute 'default nil :font (format "%s 13" my/font-family)))

(global-set-key (kbd "C-x -") 'my/decrease-font-height)
(global-set-key (kbd "C-x =") 'my/increase-font-height)
(global-set-key (kbd "C-x +") 'my/increase-font-height)

;; Navigate through buffers
(global-set-key (kbd "M-[") 'previous-buffer)
(global-set-key (kbd "M-]") 'next-buffer)

;; Fix unicode errors
(setenv "LANG" "en_US.UTF-8")
(setenv "LC_ALL" "en_US.UTF-8")
(setenv "LC_CTYPE" "en_US.UTF-8")

;; Fix size of scroll
(setq scroll-step 1
      scroll-conservatively  10000)

;; Show a marker when the line has empty characters at the end
(setq-default show-trailing-whitespace t)

;; disable `show-trailing-whitespace' for modes based in comint-mode
;; make prompt readonly
(add-hook 'comint-mode-hook #'(lambda ()
                                (setq-local show-trailing-whitespace nil)
                                (setq-local comint-prompt-read-only t)))

;; Avoid close emacs by mistake
(global-unset-key (kbd "C-x C-c"))

(defalias 'yes-or-no-p 'y-or-n-p)
(defalias 'run-elisp 'ielm)

;; place custom code generated for emacs in a separate file
(defconst custom-file (expand-file-name ".customize.el" user-emacs-directory))
(load custom-file :noerror)

Show a message with emacs startup time

(defun my/display-startup-time ()
  (message "🚀 Emacs loaded in %s with %d garbage collections."
           (float-time (time-subtract after-init-time before-init-time))
           gcs-done))

(add-hook 'emacs-startup-hook #'my/display-startup-time)

Setup fira code symbols mode.

(when (member "Fira Code" (font-family-list))
  (load-file (expand-file-name "~/.emacs.d/fira-code-mode.el"))
  (require 'fira-code-mode)
  (add-hook 'prog-mode-hook 'fira-code-mode))

Adjust font size to screen resolution, increase font size for 4K screens

(when (and (display-graphic-p) (>= (x-display-pixel-width) 3840))
  (set-face-attribute 'default nil :font (format "%s 14" my/font-family)))
(define-minor-mode big-font-mode
  "Switch between a regular font size and a presentation font size."
  :init-value nil
  :global t
  (if big-font-mode
      (progn
        ;; save current font size in a temp variable to be able to restore it
        ;; after this minor mode is disabled
        (setq big-font-mode--tmp (/ (face-attribute 'default :height) 10))
        (set-face-attribute 'default nil :font (format "%s 26" my/font-family)))
    (set-face-attribute 'default nil :font (format "%s %d" my/font-family big-font-mode--tmp))))

Parrot mode FTW!

(defun my/parrot-animate-when-compile-success (buffer result)
  (if (string-match "^finished" result)
      (parrot-start-animation)))

(use-package parrot
  :ensure t
  :config
  (parrot-mode)
  (add-hook 'before-save-hook 'parrot-start-animation)
  (add-to-list 'compilation-finish-functions 'my/parrot-animate-when-compile-success))

Open scratch buffer

(defun my/scratch-buffer()
  "Switch to scratch buffer."
  (interactive)
  (let ((buffer (get-buffer "*scratch*")))
    (if buffer
        (switch-to-buffer buffer)
      (switch-to-buffer (get-buffer-create "*scratch*"))
      (insert (substitute-command-keys initial-scratch-message)))
    (lisp-interaction-mode)))

1.1 Ansi term

For some reason ansi-term doesn't respect the global keybinding for M-] so this have to be setup in term-raw-map as well.

(with-eval-after-load 'term
  (define-key term-raw-map (kbd "M-]") 'next-buffer))

1.2 xref

Enable evil emacs state when entering a xref buffer

(with-eval-after-load 'xref
  (add-hook 'xref-after-update-hook #'(lambda () (evil-emacs-state))))

1.3 Compilation

Disable h key-binding, this has a conflict with evil-mode left navigation key-binding.

(with-eval-after-load 'compile
  ;; set cursor to follow compilation output
  (setq compilation-scroll-output t)
  ;; for elixir testing output test filename use black color which makes it ineligible
  (set-face-foreground 'ansi-color-bold "magenta")
  (define-key compilation-mode-map (kbd "g") nil)
  (define-key compilation-mode-map (kbd "r") 'recompile)
  (define-key compilation-mode-map (kbd "h") nil))

Allow to show color characters in the compilation buffer.

copied from https://stackoverflow.com/questions/3072648/cucumbers-ansi-colors-messing-up-emacs-compilation-buffer/3072831#3072831

(require 'ansi-color)

(defun colorize-compilation-buffer ()
  (let ((inhibit-read-only t))
    (ansi-color-apply-on-region (point-min) (point-max))))

(add-hook 'compilation-filter-hook 'colorize-compilation-buffer)

Custom function to select a base directory before running compilation. M-x compile always use the base directory of the buffer from where it was called as default-directory, this is awful when you want to run a project compilation command from a nested file buffer.

(defun my/compile ()
  "Run compilation process but ask for a `default-directory' before."
  (interactive)
  (let ((default-directory (read-directory-name "Base directory: " (my/project-root)))
        (cmd (read-string "Compile command: ")))
    ;; we need to "export" this variable to be able to re-run `compile' command
    (setq compile-command cmd)
    (compile compile-command)))

1.4 Narrowing

(defun my/toggle-narrowing ()
  "Toggle narrow on the selected region."
  (interactive)
  (if (buffer-narrowed-p)
      (widen)
    (if (region-active-p)
        (narrow-to-region (region-beginning) (region-end))
      (user-error "No active selection"))))

1.5 Ediff

(with-eval-after-load 'ediff
  (setq ediff-split-window-function 'split-window-horizontally)
  ;; put ediff buffer in a buffer at the bottom instead of in a new frame
  (setq ediff-window-setup-function 'ediff-setup-windows-plain))

2 Theme and styles

2.1 Dracula

(use-package dracula-theme
  :ensure t
  :config
  (load-theme 'dracula t)
  (set-face-foreground 'font-lock-variable-name-face "gray"))

2.2 Doom modeline

Enable display-battery-mode after doom-modeline is loaded.

This is required for GitHub notifications segment

(use-package async
  :ensure t)
(use-package doom-modeline
  :ensure t
  :defer t
  :custom
  (doom-modeline-modal-icon nil)
  (doom-modeline-buffer-file-name-style 'relative-from-project)
  (doom-modeline-github t)
  (doom-modeline-github-interval (* 30 60))
  :hook
  (after-init . doom-modeline-mode)
  (doom-modeline-mode . display-battery-mode))

2.3 Emoji support

(use-package unicode-fonts
  :ensure t
  :config
  (unicode-fonts-setup))

3 Dired

(with-eval-after-load "dired"
  (define-key dired-mode-map (kbd "C-c C-e") 'wdired-change-to-wdired-mode))

3.1 Nerd icons dired

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

3.2 Dired subtree

(use-package dired-subtree
  :ensure t
  :after dired
  :config
  (define-key dired-mode-map (kbd "<tab>") 'dired-subtree-toggle))

4 Editor enhancements

4.1 Whitespace

Show special markers for tab and endline characters in prog-mode

(use-package whitespace-mode
  :custom
  (whitespace-style '(tab-mark newline-mark))
  (whitespace-display-mappings '((newline-mark ?\n    [?¬ ?\n]  [?$ ?\n])
                                 (tab-mark     ?\t    [?» ?\t] [?\\ ?\t])))
  :hook
  (prog-mode . whitespace-mode))

4.2 Deactivate extended region in visual mode

Allow to visual mode work more like vim visual highlighting.

(set-face-attribute 'region nil :extend nil)

4.3 Dark and transparent title bar in macOS

(when (memq window-system '(mac ns))
  (add-to-list 'default-frame-alist '(ns-transparent-titlebar . t))
  (add-to-list 'default-frame-alist '(ns-appearance . dark)))

4.4 Share clipoard with OS

(use-package pbcopy
  :ensure t)

4.5 Highlight TODO, FIXME, etc

(defun my/highlight-todo-like-words ()
  (font-lock-add-keywords
   nil `(("\\<\\(FIXME\\|TODO\\)"
          1 font-lock-warning-face t))))

(add-hook 'prog-mode-hook 'my/highlight-todo-like-words)

4.6 Auto fill mode

Use auto-fill-mode only for comments and only with programming buffers

(setq comment-auto-fill-only-comments t)

(add-hook 'prog-mode-hook #'(lambda ()
                              (auto-fill-mode 1)))

4.7 Load PATH environment

exec-path-from-shell by default uses ("-l" "-i") when starts a new shell to get the PATH, -i option was removed to open a non interactive shell so it can be faster at startup.

(use-package exec-path-from-shell
  :ensure t
  :custom
  (exec-path-from-shell-arguments '("-l"))
  (exec-path-from-shell-check-startup-files nil)
  :config
  (when (memq window-system '(mac ns))
    (exec-path-from-shell-initialize)))

4.8 Editorconfig

(use-package editorconfig
  :ensure t
  :config
  (editorconfig-mode 1))

4.9 Snippets

(use-package yasnippet
  :ensure t
  :hook ((prog-mode . yas-minor-mode)
         (conf-mode . yas-minor-mode)
         (text-mode . yas-minor-mode)
         (snippet-mode . yas-minor-mode)))

(use-package yasnippet-snippets
  :ensure t
  :after (yasnippet))

4.10 Wakatime

(use-package wakatime-mode
  :ensure t
  :if (executable-find "wakatime-cli")
  :init
  (setq wakatime-cli-path (executable-find "wakatime-cli"))
  :config
  (global-wakatime-mode))

4.11 Highlight thing

(use-package highlight-thing
  :ensure t
  :hook
  (prog-mode . highlight-thing-mode))

4.12 Various changes

Disable lock files

(setq create-lockfiles nil)

4.13 Reformatter

(use-package reformatter
  :ensure t)

4.14 Vterm

Function to search into zsh history

(defun my/select-from-zsh-history ()
  "Selectt one option from ~/.zsh_history file."
  (with-temp-buffer
    (insert-file-contents (expand-file-name "~/.zsh_history"))
    (let* ((raw-content (buffer-substring-no-properties (point-min) (point-max)))
           (lines (string-split raw-content "\n"))
           (choices (mapcar (lambda (line) (second (string-split line ";"))) lines)))
      (completing-read "Select command: " choices))))

(defun my/insert-from-zsh-history ()
  "Search into zsh history and insert the selected choice into buffer."
  (interactive)
  (when-let ((selected-choice (my/select-from-zsh-history)))
    (vterm-insert selected-choice)))
(use-package vterm
  :ensure t
  :defer t
  :hook
  (vterm-mode . (lambda ()
                  (setq-local show-trailing-whitespace nil)))
  :custom
  (vterm-module-cmake-args "-DUSE_SYSTEM_LIBVTERM=yes")
  (vterm-always-compile-module t))

4.15 Toggle terminal

'project use always the same terminal per project, this way we avoid to create a new terminal for each call to vterm-toggle. 'reset-window-configration yes, it's suppose to be configration, for some reason it was defined like this instead of configuration

Also for easy access insert mode is activated right away after vterm is shown

(use-package vterm-toggle
  :ensure t
  :custom
  (vterm-toggle-scope 'project)
  (vterm-toggle-hide-method 'reset-window-configration)
  :hook
  (vterm-toggle-show . evil-insert-state))

4.16 iSpell

Avoid check spelling in markdown code blocks

(with-eval-after-load 'ispell
  (setq ispell-program-name "aspell")
  (add-to-list 'ispell-skip-region-alist
               '("^```" . "^```")))

When editing a commit message ispell should ignore lines that start with #, these lines are diff details about the commit.

(defun my/setup-ispell-for-commit-message ()
  "Setup `ispell-skip-region-alist' to avoid lines starting with #.
  This way diff code will be ignored when ispell run."
  (setq-local ispell-skip-region-alist (cons '("^#" . "$") ispell-skip-region-alist)))

4.17 Tree sitter

Incremental code parsing for better syntax highlighting

(use-package tree-sitter
  :ensure t
  :hook
  (tree-sitter-after-on . tree-sitter-hl-mode)
  :config
  (global-tree-sitter-mode))

(use-package tree-sitter-langs
  :ensure t)

Run ispell in text nodes

(use-package tree-sitter-ispell
  :ensure t
  :defer t
  :bind (("C-x C-s" . tree-sitter-ispell-run-at-point)))

5 Evil

(defun my/find-file-under-cursor ()
  "Check it the filepath under cursor is an absolute path otherwise open `project-find-file' and insert the filepath."
  (interactive)
  (let ((file-path (thing-at-point 'filename t)))
    (if (file-name-absolute-p file-path)
        (find-file-at-point file-path)
      (minibuffer-with-setup-hook #'(lambda () (insert file-path))
        (project-find-file)))))
(use-package evil
  :ensure t
  :init
  (setq evil-emacs-state-cursor '("white" box)
        evil-normal-state-cursor '("green" box)
        evil-visual-state-cursor '("orange" box)
        evil-insert-state-cursor '("red" bar)
        ;; use emacs-28 undo system
        evil-undo-system 'undo-redo)
  :config
  (evil-mode 1)
  (modify-syntax-entry ?_ "w")
  (define-key evil-normal-state-map (kbd "C-p") 'diff-hl-previous-hunk)
  (define-key evil-normal-state-map (kbd "C-n") 'diff-hl-next-hunk)
  (define-key evil-normal-state-map "gf" 'my/find-file-under-cursor)
  (add-hook 'prog-mode-hook #'(lambda ()
                                (modify-syntax-entry ?_ "w")))

  ;; Setup leader key only for `normal', `visual' and `motion' modes
  (evil-set-leader '(normal visual motion) (kbd "\\"))

  (evil-define-key nil 'global (kbd "<leader>SPC") #'(lambda ()
                                                       (interactive)
                                                       (call-interactively #'execute-extended-command)))
  (evil-define-key nil 'global (kbd "<leader>a") #'(lambda ()
                                                     (interactive)
                                                     (if (region-active-p)
                                                         (my/grep-in-project (buffer-substring-no-properties (region-beginning) (region-end)))
                                                       (my/grep-in-project (thing-at-point 'symbol)))))
  (evil-define-key nil 'global (kbd "<leader>A") 'my/grep-in-project)
  (evil-define-key nil 'global (kbd "<leader>ba") 'my/add-bookmark)
  (evil-define-key nil 'global (kbd "<leader>bb") 'my/bookmark-switch)
  (evil-define-key nil 'global (kbd "<leader>B") #'(lambda ()
                                                     (interactive)
                                                     (call-interactively #'switch-to-buffer)))
  (evil-define-key nil 'global (kbd "<leader>c") 'vterm-toggle)
  (evil-define-key nil 'global (kbd "<leader>e") 'my/find-file-in-project)
  (evil-define-key nil 'global (kbd "<leader>f") 'find-file)
  (evil-define-key nil 'global (kbd "<leader>g") 'magit-status)
  (evil-define-key nil 'global (kbd "<leader>G") 'magit-file-dispatch)
  (evil-define-key nil 'global (kbd "<leader>i") 'consult-imenu)
  (evil-define-key nil 'global (kbd "<leader>hs") 'diff-hl-stage-current-hunk)
  (evil-define-key nil 'global (kbd "<leader>hk") 'diff-hl-revert-hunk)
  (evil-define-key nil 'global (kbd "<leader>k") 'kill-buffer)
  (evil-define-key nil 'global (kbd "<leader>l") 'display-line-numbers-mode)
  (evil-define-key nil 'global (kbd "<leader>n") 'evil-buffer-new)
  (evil-define-key nil 'global (kbd "<leader>N") 'my/toggle-narrowing)
  (evil-define-key nil 'global (kbd "<leader>pa") 'my/copy-abs-path)
  (evil-define-key nil 'global (kbd "<leader>pr") 'my/copy-relative-path)
  (evil-define-key nil 'global (kbd "<leader>q") 'consult-line)
  (evil-define-key nil 'global (kbd "<leader>r") 'my/replace-at-point-or-region)
  (evil-define-key nil 'global (kbd "<leader>R") 'recompile)
  (evil-define-key nil 'global (kbd "<leader>s") 'my/toggle-spanish-characters)
  (evil-define-key nil 'global (kbd "<leader>t") 'persp-switch)
  (evil-define-key nil 'global (kbd "<leader>w") 'my/toggle-maximize)
  (evil-define-key nil 'global (kbd "<leader>x") 'my/resize-window)
  (evil-define-key nil 'global (kbd "<leader>y") 'consult-yank-from-kill-ring)

  (face-spec-set
   'evil-ex-substitute-matches
   '((t :foreground "red"
        :strike-through t
        :weight bold)))

  (face-spec-set
   'evil-ex-substitute-replacement
   '((t
      :foreground "green"
      :weight bold))))

(use-package evil-nerd-commenter
  :ensure t
  :after (evil)
  :config
  (global-set-key (kbd "C-\-") 'evilnc-comment-operator)
  ;; avoid to auto-setup of keybindings
  (setq evilnc-use-comment-object-setup nil))

(use-package evil-surround
  :ensure t
  :after (evil)
  :config
  (global-evil-surround-mode 1))

(defun my/replace-at-point-or-region ()
  "Setup buffer replace string for word at point or active region using evil ex mode."
  (interactive)
  (let ((text (if (region-active-p)
                  (buffer-substring-no-properties (region-beginning) (region-end))
                (word-at-point))))
    (evil-ex (concat "%s/" text "/"))))

(use-package evil-matchit
  :ensure t
  :config (global-evil-matchit-mode 1))

6 IA models integration

Integration with different "backends", ollama, openai, and so on.

(use-package llm
  :ensure t)

UI to interact with models, relies on llm

(use-package ellama
  :ensure t
  :custom
  (ellama-language "English")
  :config
  (require 'llm-ollama)
  (with-eval-after-load 'llm-ollama)
  (setopt ellama-provider (make-llm-ollama
                           :host "localhost"
                           :chat-model "zephyr")))

Custom functions to better management of models

(defun my/switch-ollama-provider ()
  "Switch ollama provider by using the installed local models."
  (interactive)
  (let* ((raw-result (shell-command-to-string "ollama list | awk '{print $1}' | tail -n+2"))
         (choices (string-split (string-trim raw-result) "\n"))
         (choices (mapcar (lambda (choice) (car (string-split choice ":"))) choices))
         (model (completing-read "Choose model" choices)))
    (setopt ellama-provider (make-llm-ollama :host "localhost" :chat-model model))
    (message "Model %s configured as ollama provider." (propertize model 'face '(:foreground "magenta")))))

7 Utils

7.1 Which-key

(use-package which-key
  :ensure t
  :config
  (which-key-mode)
  (which-key-setup-minibuffer))

7.2 Auto pair

Complete parenthesis, square brackets, etc

Enable it globally and disable it just for org-mode to avoid having a conflict with <s

(electric-pair-mode)
(add-hook 'org-mode-hook #'(lambda ()
                             (electric-pair-local-mode -1)))

7.3 Restclient

(use-package restclient
  :ensure t
  :defer t
  :mode (("\\.http\\'" . restclient-mode))
  :bind (:map restclient-mode-map
              ("C-c C-h" . 'cloak-mode)
              ("C-c C-f" . 'json-pretty-print))) ;TODO: change to only apply json formatting when the content-type is application/json

(use-package company-restclient
  :ensure t
  :after (restclient)
  :config
  (add-to-list 'company-backends 'company-restclient))

7.4 Rainbow delimiters

(use-package rainbow-delimiters
  :ensure t
  :hook
  (prog-mode . rainbow-delimiters-mode))

7.5 XML formatter

(reformatter-define xml-format
  :program "xmlformat"
  :group 'xml)

(with-eval-after-load 'nxml-mode
  (define-key nxml-mode-map (kbd "C-c C-f") 'xml-format-buffer))

7.6 SQL formatter

Install pgformatter using homebrew brew install pgformatter

(reformatter-define sql-format
  :program "pg_format")

(defun my/format-sql ()
  "Format active region otherwise format the entire buffer."
  (interactive)
  (if (region-active-p)
      (sql-format-region (region-beginning) (region-end))
    (sql-format-buffer)))

(with-eval-after-load 'sql
  (add-hook 'sql-mode-hook 'flymake-sqlfluff-load)
  (add-hook 'sql-mode-hook 'flymake-mode)
  (define-key sql-mode-map (kbd "C-c C-f") 'my/format-sql))

7.7 SQL linter using sqlfluff

(use-package flymake-sqlfluff
  :ensure t)

8 Common packages

Used in every major mode

8.1 Company

(use-package company
  :ensure t
  :init
  (setq company-idle-delay 0.1
        company-tooltip-limit 10
        company-minimum-prefix-length 3)
  :hook (after-init . global-company-mode)
  :config
  (define-key company-active-map (kbd "C-n") 'company-select-next)
  (define-key company-active-map (kbd "C-p") 'company-select-previous))

8.2 Flymake

Only activate flymake for actual projects and for prog-mode

(defun my/setup-flymake ()
  "Activate flymake only if we are inside a project."
  (when (functionp 'my/project-p)
    (flymake-mode 1)))

(add-hook 'prog-mode-hook 'my/setup-flymake)

(with-eval-after-load "flymake"
  (define-key flymake-mode-map (kbd "M-n") 'flymake-goto-next-error)
  (define-key flymake-mode-map (kbd "M-p") 'flymake-goto-prev-error))

8.3 Direnv

Handle environment variables per buffer usiong a .envrc file.

(use-package envrc
  :ensure t
  :config
  (envrc-global-mode)
  :bind (:map envrc-mode-map
              ("C-c C-h" . 'cloak-mode)))

8.4 Cloak mode

Hide values that match regex patterns in .envrc and restclient files

(use-package cloak-mode
 :ensure t
 :custom
 (cloak-mode-patterns '((envrc-file-mode . "[a-zA-Z0-9_]+[ \t]*=[ \t]*\\(.*+\\)$")
                        (restclient-mode . "^:[^: ]+[ \t]*=[ \t]*\\(.+?\\)$")))
 (cloak-mode-mask "🙈🙈🙈")
 :config
 (global-cloak-mode))

8.5 perspective.el

(use-package perspective
  :ensure t
  :config
  (persp-mode)
  ;; change default font-face color to be aligned with doom-mode-line
  (set-face-foreground 'persp-selected-face "green")
  ;; setup vim tab like key-bindings
  (define-key evil-normal-state-map (kbd "gt") 'persp-next)
  (define-key evil-normal-state-map (kbd "gT") 'persp-prev))

8.6 Project.el

project.el default prefix is C-x

(defun my/project-edit-dir-locals ()
  "Edit .dir-locals.el file in project root."
  (interactive)
  (find-file (expand-file-name ".dir-locals.el" (my/project-root))))

(defun my/project-edit-direnv ()
  "Edit .envrc file in project root."
  (interactive)
  (find-file (expand-file-name ".envrc" (my/project-root))))

(use-package project
  :straight (:type built-in)
  :bind (:map project-prefix-map
              ("D" . 'my/project-edit-direnv)
              ("d" . 'project-dired)
              ("e" . 'my/project-edit-dir-locals)
              ("k" . 'my/project-kill-buffers)
              ("n" . 'my/project-open-new-project)
              ("p" . 'my/project-switch)))

Define helper functions to be used by other packages

(defun my/project-root ()
  "Return project root path."
  (project-root (project-current)))

(defun my/project-p ()
  (project-current))

(defun my/project-name ()
  "Get project name extracting latest part of project path."
  (if (my/project-p)
      (second (reverse (split-string (my/project-root) "/")))
    nil))

perspective.el integration, a new perspective should be "attached" to a project so it's easy to switch between them.

(defun my/project-switch ()
  "Switch to a project and trigger switch action."
  (interactive)
  ;; make sure all the projects list is available to be used
  (project--ensure-read-project-list)
  (let* ((projects (mapcar 'car project--list))
         (choice (completing-read "Switch to project: " projects))
         (default-directory choice))
    ;; `default-directory' must be defined so `project.el' can know is in a new project
    (my/project-switch-action)))

(defun my/project-switch-action ()
  "Switch to a new perspective which name is project's name and open `project-find-file'."
  (interactive)
  (persp-switch (my/project-name))
  (project-find-file))

(defun my/project-kill-buffers ()
  "Kill all the related buffers to the current project and delete its perspective as well."
  (interactive)
  (let* ((project-name (my/project-name))
         (project (project-current))
         (buffers-to-kill (project--buffers-to-kill project)))
    (when (yes-or-no-p (format "Kill %d buffers in %s?" (length buffers-to-kill) (my/project-root)))
      ;; in case we're using eglot we shutdown its server
      (if (and (featurep 'eglot) (eglot-managed-p))
          (eglot-shutdown (eglot-current-server)))
      (mapc #'kill-buffer buffers-to-kill)
      (persp-kill project-name))))

(defun my/project-open-new-project ()
  "Open a project for the first time and add it to `project.el' projects list."
  (interactive)
  (let* ((project-path-abs (read-directory-name "Enter project root: "))
         ;; we need to define `default-directory' to be able to get the new project when `project-current' is called
         (default-directory (replace-regexp-in-string (expand-file-name "~") "~" project-path-abs)))
    (project-remember-project (project-current))
    (my/project-switch-action)))

8.7 Completion

UI for completion

(use-package vertico
  :ensure t
  :init
  (vertico-mode)
  :custom
  ;; fixed height
  (vertico-resize nil)
  ;; show max 15 elements
  (vertico-count 15)
  :config
  ;; `C-;' will open embark and `o' with execute `find-file-other-window'
  (define-key vertico-map (kbd "C-<return>") (kbd "C-; o")))

Load vertico-multiform which is required for vertico-posframe

(use-package vertico-multiform
  :after vertico
  :straight nil
  :load-path "straight/repos/vertico/extensions/")

Vertico posframe, show all the candidates in a child-frame, it will activated only for GUI version.

(use-package vertico-posframe
  :ensure t
  :if (display-graphic-p)
  :init
  (vertico-posframe-mode 1))

Add annotations to results shown by vertico

(use-package marginalia
  :ensure
  :init
  (marginalia-mode))

Icons support

(use-package nerd-icons-completion
  :ensure t
  :after marginalia
  :config
  (nerd-icons-completion-mode)
  (add-hook 'marginalia-mode-hook #'nerd-icons-completion-marginalia-setup))

Enable better completion styles

(use-package orderless
  :ensure t
  :config
  (setq completion-styles '(orderless basic)
        completion-category-overrides '((file (styles basic partial-completion)))))

Disable orderless completion style in company to keep previous behaviour which I like, this was copied from orderless documentation.

;; We follow a suggestion by company maintainer u/hvis:
;; https://www.reddit.com/r/emacs/comments/nichkl/comment/gz1jr3s/
(defun company-completion-styles (capf-fn &rest args)
  (let ((completion-styles '(basic partial-completion)))
    (apply capf-fn args)))

(advice-add 'company-capf :around #'company-completion-styles)

Search into project source

(use-package consult
  :ensure t
  :config
  ;; Use consult for completion inside minibuffer, for example when
  ;; searching for a file.
  (setq completion-in-region-function #'consult-completion-in-region))

Integration with yasnippets

(use-package consult-yasnippet
  :ensure t
  :defer t)

Helpers to search term at point and general search into project

(defun my/grep-in-project (&optional term)
  "Run grep in current project.
If TERM is not nil it will be used as initial value."
  (interactive)
  (let* ((pattern (read-string "Pattern: " (or term "")))
         ;; add an extra space to be able to start typing more filters
         (pattern (concat pattern " ")))
    (call-interactively #'(lambda ()
                            (interactive)
                            (consult-ripgrep (my/project-root) pattern)))))

Integration with embark

(use-package embark
  :ensure t
  :bind
  ("C-;" . embark-act)
  :config
  ;; grep exported data can have a lot of white spaces so we don't want
  ;; them to be shown while editing their content
  (setq-mode-local embark-collect-mode show-trailing-whitespace nil))

(use-package embark-consult
  :ensure t
  :defer t)

(defun my/edit-completing-results ()
  "Use results origin to execute an action after export them with `embark-export'."
  (interactive)
  ;; call of `project-find-file'
  (when (cl-search "Find file in" (buffer-string))
    (run-at-time 0 nil #'embark-export)
    (run-at-time 0 nil #'wdired-change-to-wdired-mode)
    (run-at-time 0 nil #'evil-normal-state))
  ;; call of `consult-ripgrep'
  (when (cl-search "Ripgrep" (buffer-string))
    ;; we use `run-at-time' to ensure all of these steps
    ;; will be executed in order
    (run-at-time 0 nil #'embark-export)
    (run-at-time 0 nil #'wgrep-change-to-wgrep-mode)
    (run-at-time 0 nil #'evil-normal-state)))

(define-key minibuffer-mode-map (kbd "C-c C-e") #'my/edit-completing-results)

Edit grep buffer

(use-package wgrep
  :ensure t
  :custom
  (wgrep-auto-save-buffer t)
  :bind
  ("C-c C-c" . 'wgrep-finish-edit)
  ("C-c C-k" . 'wgrep-abort-changes))

9 Emacs process list

(defun my/kill-emacs-process ()
  "Show a list of current Emacs processes and kill the selected one."
  (interactive)
  (let* ((names (mapcar #'process-name (process-list)))
         (process-name (completing-read "Choose process: " names)))
    (delete-process (get-process process-name))
    (message "%s process killed" (propertize process-name 'face '(:foreground "magenta")))))

(global-set-key (kbd "C-x c p") 'my/kill-emacs-process)

10 Git backup

Save a backup on every save, also allow to recover any version of a file

(defvar my/backup-dir (expand-file-name "~/.git-backup"))

(defun my/git-backup-versioning ()
  "Save a version of the current file."
  (unless (featurep 'git-backup)
    (require 'git-backup))
  (git-backup-version-file (executable-find "git") my/backup-dir '() (buffer-file-name)))

(defun my/git-backup-run-action (command commit-hash)
  "Execute COMMAND with COMMIT-HASH using another defaults arguments."
  (apply command `(,(executable-find "git") ,my/backup-dir ,commit-hash ,(buffer-file-name))))

(defun my/git-backup-sort (completions)
  "Given COMPLETIONS define a custom sort function."
  (lambda (string pred action)
    (if (eq action 'metadata)
        '(metadata (display-sort-function . identity))
      (complete-with-action action completions string pred))))

(defun my/git-backup ()
  "Navigate in versions of the current file."
  (interactive)
  (unless (featurep 'git-backup)
    (require 'git-backup))
  ;; for some reason an extra space after `%h|' is required to avoid an error when
  ;; the shell command is executed
  (let* ((candidates (git-backup-list-file-change-time (executable-find "git") my/backup-dir "%cI|%h| %ar" (buffer-file-name)))
         (selection (completing-read "Pick revision: " (my/git-backup-sort candidates)))
         (commit-hash (nth 1 (string-split selection "|")))
         (action (completing-read "Choose action: " '("diff" "new buffer" "replace current buffer"))))
    (cond ((string-equal action "diff") (my/git-backup-run-action 'git-backup-create-ediff commit-hash))
          ((string-equal action "new buffer") (my/git-backup-run-action 'git-backup-open-in-new-buffer commit-hash))
          ((string-equal action "replace current buffer") (my/git-backup-run-action 'git-backup-replace-current-buffer commit-hash))
          (t (message "Not valid option")))))

(use-package git-backup
  :ensure t
  :hook (after-save . my/git-backup-versioning))

11 Meme

This package requires to have svg support in emacs, this feature relies on librsvg at compilation time

(defun my/meme-from-clipboard ()
  "Create a meme using an image from clipboard"
  (interactive)
  (unless (executable-find "pngpaste")
    (user-error "please install pngpaste"))

  (let* ((filepath (make-temp-file "clipboard" nil ".png"))
         (command (format "pngpaste %s" filepath))
         (command-stdout (shell-command-to-string command)))
    ;; pngpaste returns "" when found a valid image in the clipboard
    (unless (string-equal command-stdout "")
      (user-error (string-trim command-stdout)))

    (switch-to-buffer (get-buffer-create "*meme*"))
    (meme-mode)
    (meme--setup-image filepath)))
(use-package imgur
  :ensure t
  :defer t
  :straight (imgur
             :type git
             :host github
             :repo "myuhe/imgur.el"))

(use-package meme
  :ensure t
  :defer t
  :commands (meme-mode meme)
  :straight (meme
             :type git
             :host github
             :repo "larsmagne/meme")
  :config
  ;; fix to be able to read images, straight.el put files in a different directory so we have to
  ;; move them to the right one
  (let ((images-dest-dir (concat user-emacs-directory "straight/build/meme/images"))
        (images-source-dir (concat user-emacs-directory "straight/repos/meme/images")))
    (unless (file-directory-p images-dest-dir)
      (shell-command (format "cp -r %s %s" images-source-dir images-dest-dir)))))

12 Orgmode

Configured variables:

  • org-latex-caption-above puts table captions at the bottom
  • org-clock-persist persists time even if emacs is closed
  • org-src-fontify-natively enables syntax highlighting for code blocks
  • org-log-done saves the timestamp when a task is done
  • org-src-preserve-indentation when is t avoid to insert a left indentation in source blocks
(defun my/org-insert-image-from-clipboard ()
  "Insert image from clipboard using an org tag"
  (interactive)
  (let* ((image-name (read-string "Filename: " "image.png"))
         (images-folder "./images")
         (image-path (format "%s/%s" images-folder image-name)))
    (unless (file-directory-p images-folder)
      (shell-command (format "mkdir -p %s" images-folder)))
    (shell-command (format "pngpaste %s" image-path))
    (insert (format "[[file:%s]]" image-path))))

(evil-define-key nil org-mode-map (kbd "<leader>mii") 'my/org-insert-image-from-clipboard)

When I read books on Apple Books and I want to insert some quote Apple Books insert some text I don't want in my notes, this function delete that and just insert the meaning part using org quote syntax.

(defun my/org-insert-quote-from-apple-books ()
  "Take quote from clipboard and remove all the unnecesary text and insert
    an org quote in the current position"
  (interactive)
  (let* ((raw-value (current-kill 0 t))
         (tmp (second (split-string raw-value "“")))
         (quote-value (car (split-string tmp "”"))))
    (insert "#+begin_quote\n")
    (insert (concat quote-value "\n"))
    (insert "#+end_quote\n")))
(defvar my/org-src-block-tmp-window-configuration nil)

(defun my/org-edit-special (&optional arg)
  "Save current window configuration before a org-edit buffer is open."
  (setq my/org-src-block-tmp-window-configuration (current-window-configuration)))

(defun my/org-edit-src-exit ()
  "Restore the window configuration that was saved before org-edit-special was called."
  (set-window-configuration my/org-src-block-tmp-window-configuration))

(with-eval-after-load 'org
  (setq org-latex-caption-above nil
        org-clock-persist 'history
        org-src-fontify-natively t
        org-src-preserve-indentation t
        org-log-done t)
  (org-clock-persistence-insinuate)

  (add-hook 'org-mode-hook #'(lambda ()
                               ;; Since emacs 27 this is needed to use shortcuts like <s to create source blocks
                               (unless (featurep 'org-tempo)
                                 (require 'org-tempo))
                               (org-indent-mode t)))

  (advice-add 'org-edit-special :before 'my/org-edit-special)
  (advice-add 'org-edit-src-exit :after 'my/org-edit-src-exit)

  (org-babel-do-load-languages 'org-babel-load-languages
                               '((python . t)
                                 (shell . t)
                                 (lisp . t)
                                 (sql . t)
                                 (dot . t)
                                 (plantuml . t)
                                 (emacs-lisp . t))))

(use-package htmlize
  :ensure t
  :after (org))

Fix error with TAB in evil-mode in org-mode with org elements.

(defun my/org-tab ()
  "Run `org-cycle' only at point of an org element."
  (interactive)
  (if (org-element-at-point)
      (org-cycle)
    (evil-jump-forward)))

(with-eval-after-load 'org
  (define-key org-mode-map (kbd "<tab>") 'my/org-tab))

12.1 Org tree slide

A tool to show org file as an slideshow

hide-mode-line hide the modeline to allow to have a clean screen while using org-tree-slide-mode

(use-package hide-mode-line
  :ensure t)

Some tweaks to have a better looking while presenting slides

(defun my/org-tree-slide-setup ()
  (org-display-inline-images)
  (hide-mode-line-mode 1))

(defun my/org-tree-slide-end ()
  (org-display-inline-images)
  (hide-mode-line-mode 0))

(use-package org-tree-slide
  :ensure t
  :defer t
  :custom
  (org-image-actual-width nil)
  (org-tree-slide-activate-message "Presentation started!")
  (org-tree-slide-deactivate-message "Presentation finished!")
  :hook ((org-tree-slide-play . my/org-tree-slide-setup)
         (org-tree-slide-stop . my/org-tree-slide-end))
  :bind (:map org-tree-slide-mode-map
              ("C-<" . org-tree-slide-move-previous-tree)
              ("C->" . org-tree-slide-move-next-tree)))

13 Denote

Note taking using denote

(use-package denote
  :ensure t
  :custom (denote-directory "~/Documents/wiki")
  :hook ((dired-mode . denote-dired-mode)))

(defun my/wiki ()
  "Open personal wiki and launch Dired."
  (interactive)
  (dired (expand-file-name "~/Documents/wiki"))
  (dired-hide-details-mode t))

14 Latex

(use-package auctex
  :ensure t
  :defer t)

(use-package latex-preview-pane
  :ensure t
  :defer t)

15 Git

15.1 Git-link

Open selected region in remote repo page

(use-package git-link
  :ensure t
  :defer t)

15.2 Git modes

This pacakge includes gitignore-mode, gitconfig-mode and gitattributes-mode

(use-package git-modes
  :defer t
  :ensure t)

15.3 Magit

(defun my/magit-blame-quit ()
  "Restore evil state after magit blame mode is closed."
  (evil-exit-emacs-state))

(use-package magit
  :ensure t
  :custom
  ;; restore previous window configuration after a buffer is closed
  (magit-bury-buffer-function 'magit-restore-window-configuration)
  ;; open magit status buffer in the whole frame
  (magit-display-buffer-function 'magit-display-buffer-fullframe-status-v1)
  :defer t
  :config
  (advice-add 'magit-blame-quit :after 'my/magit-blame-quit)
  (add-hook 'git-commit-mode-hook 'my/setup-ispell-for-commit-message)
  (add-hook 'magit-blame-mode-hook #'(lambda () (evil-emacs-state))))

15.4 Magit delta

Use delta tool to show diffs in magit

(use-package magit-delta
  :ensure t
  :if (executable-find "delta")
  :hook (magit-mode . magit-delta-mode))

15.5 Forge

(use-package forge
  :ensure t
  :after (magit closql)
  :config
  (add-hook 'forge-topic-mode-hook #'(lambda () (evil-emacs-state))))

15.6 Git diff-hl

(use-package diff-hl
  :ensure t
  :custom
  (diff-hl-show-staged-changes nil)
  ;; for some reason the :hook form doesn't work so we have to use :init
  :init
  (add-hook 'magit-pre-refresh-hook 'diff-hl-magit-pre-refresh)
  (add-hook 'magit-post-refresh-hook 'diff-hl-magit-post-refresh)
  (add-hook 'dired-mode-hook 'diff-hl-dired-mode)
  :config
  (global-diff-hl-mode))

15.7 Timemachine

(use-package git-timemachine
  :ensure t
  :config
  (add-hook 'git-timemachine-mode-hook #'(lambda () (evil-emacs-state))))

15.8 Gist

(use-package gist
  :ensure t
  :defer t)

15.9 Linkode

(use-package linkode
  :ensure t
  :defer t)

16 Web

16.1 Web mode

(defun my/web-mode-hook ()
  (emmet-mode)
  (rainbow-delimiters-mode-disable))

(use-package web-mode
  :ensure t
  :custom
  (web-mode-enable-current-element-highlight t)
  (web-mode-enable-current-column-highlight t)
  :mode (("\\.html\\'" . web-mode)
         ("\\.html.eex\\'" . web-mode)
         ("\\.html.leex\\'" . web-mode)
         ("\\.hbs\\'" . web-mode))
  :config
  (add-hook 'web-mode-hook 'my/web-mode-hook))

16.2 Emmet

(use-package emmet-mode
  :ensure t)

16.3 Sass

(use-package sass-mode
  :ensure t
  :defer t)

16.4 Rainbow

(use-package rainbow-mode
  :ensure t
  :hook
  ((css-mode . rainbow-mode)
   (sass-mode . rainbow-mode)
   (scss-mode . rainbow-mode)))

17 Miscellaneous

(use-package writeroom-mode
  :ensure t)

(use-package csv-mode
  :ensure t
  :defer t)

(use-package jsonian
  :ensure t
  :defer t)

(use-package request
  :ensure t
  :defer t)

(use-package graphql-mode
  :ensure t
  :defer t)

(reformatter-define terraform-format
  :program "terraform"
  :args '("fmt" "-")
  :group 'terraform)

(use-package terraform-mode
  :ensure t
  :defer t
  :bind (:map terraform-mode-map
              ("C-c C-f" . 'terraform-format-buffer)))

(defun my/k8s-apply ()
  "Apply current yaml file to the current kubernetes context."
  (interactive)
  (let ((default-directory (file-name-directory buffer-file-name)))
    (compile (format "kubectl apply -f %s" buffer-file-name))))

(defun my/k8s-delete ()
  "Delete current yaml file to the current kubernetes context."
  (interactive)
  (let ((default-directory (file-name-directory buffer-file-name)))
    (compile (format "kubectl delete -f %s" buffer-file-name))))

(use-package yaml-mode
  :ensure t
  :bind (:map yaml-mode-map
              ("C-c C-c" . 'my/k8s-apply)
              ("C-c C-d" . 'my/k8s-delete)))

(use-package flymake-yamllint
  :ensure t
  :defer t
  :hook
  (yaml-mode . flymake-mode)
  (yaml-mode . flymake-yamllint-setup))

(use-package yaml-pro
  :ensure t
  :defer t
  :hook
  (yaml-mode . yaml-pro-ts-mode)
  :config
  ;; this binding conflicts with org indirect mode
  (define-key yaml-pro-ts-mode-map (kbd "C-c '") nil))

(use-package hcl-mode
  :ensure t)

;; Used for gherkin files (.feature)
(use-package feature-mode
  :ensure t
  :defer t)

(use-package toml-mode
  :ensure t
  :defer t)

(use-package nix-mode
  :ensure t
  :defer t
  :mode "\\.nix\\'")

(use-package mermaid-mode
  :ensure t
  :defer t)

(use-package markdown-mode
  :ensure t
  :defer t
  :config
  (add-hook 'markdown-mode-hook #'(lambda ()
                                    (setq-local fill-column 120)
                                    (auto-fill-mode 1))))

(use-package edit-indirect
  :ensure t
  :defer t)

(use-package dockerfile-mode
  :ensure t
  :defer t)

(use-package dumb-jump
  :ensure t
  :defer t
  :custom
  (dumb-jump-force-searcher 'rg)
  (dumb-jump-selector 'completing-read))

helpful, enhance help functions

(use-package helpful
  :ensure t
  :defer t)

;; these function have autoload annotation so they will load `helpful' package when they are called
;; because we're defined just keybindings we can just use the symbol even the function is not loaded yet
(global-set-key (kbd "C-h f") #'helpful-callable)
(global-set-key (kbd "C-h v") #'helpful-variable)
(global-set-key (kbd "C-h k") #'helpful-key)

Use ESC key instead C-g to close and abort

Copied from somewhere

(defun minibuffer-keyboard-quit ()
  "Abort recursive edit.
  In Delete Selection mode, if the mark is active, just deactivate it;
  then it takes a second \\[keyboard-quit] to abort the minibuffer."
  (interactive)
  (if (and delete-selection-mode transient-mark-mode mark-active)
      (setq deactivate-mark  t)
    (when (get-buffer "*Completions*") (delete-windows-on "*Completions*"))
    (abort-recursive-edit)))

(with-eval-after-load 'evil
  (define-key evil-normal-state-map [escape] 'keyboard-quit)
  (define-key evil-visual-state-map [escape] 'keyboard-quit))

(define-key minibuffer-local-map [escape] 'minibuffer-keyboard-quit)
(define-key minibuffer-local-ns-map [escape] 'minibuffer-keyboard-quit)
(define-key minibuffer-local-completion-map [escape] 'minibuffer-keyboard-quit)
(define-key minibuffer-local-must-match-map [escape] 'minibuffer-keyboard-quit)
(define-key minibuffer-local-isearch-map [escape] 'minibuffer-keyboard-quit)
(global-set-key [escape] 'evil-exit-emacs-state)

Emacs Start Up Profiler

(use-package esup
  :ensure t)

18 LSP

(use-package eglot
  :ensure nil
  :defer t
  :straight (:type built-in)
  :bind (:map eglot-mode-map
              ("C-c C-d" . 'eldoc-doc-buffer)
              ("C-c C-s" . 'xref-find-references))
  :config
  (setf (alist-get 'elixir-mode eglot-server-programs) `(,(expand-file-name "~/Code/oss/elixir-ls/release/language_server.sh"))))

In case we don't have eglot running we can relay on dump-jump

(defun my/goto-definition-dumb-jump-fallback ()
  "Go to definition using eglot when is active otherwise use dumb-jump."
  (interactive)
  (if (and (featurep 'eglot) (eglot-managed-p))
      (xref-find-definitions (thing-at-point 'symbol))
    (dumb-jump-go)))

19 Programming languages

19.1 Shell scripts

(use-package flymake-shellcheck
  :ensure t
  :defer t
  :if (executable-find "shellcheck")
  :commands flymake-shellcheck-load
  :init
  (add-hook 'sh-mode-hook 'flymake-shellcheck-load))

Bash formatter using shfmt

(reformatter-define sh-format
  :program "shfmt"
  :args '("-i" "2" "-")
  :group 'sh)

(with-eval-after-load 'sh-script
  (define-key sh-mode-map (kbd "C-c C-f") 'sh-format-buffer))

19.2 C

clang-format is required for this, we can install it with brew install clang-format

(reformatter-define c-format
  :program "clang-format")

(with-eval-after-load 'cc-mode
  (define-key c-mode-map (kbd "C-c C-f") 'c-format-buffer))

19.3 Python

For each virtual environment install the following packages:

pip install elpy jedi flake8 epc

For LSP support python-language-server is required

pip python-language-server

Install flymake-ruff

(use-package flymake-ruff
  :ensure t)
(reformatter-define python-ruff-format
  :program "ruff"
  :args '("format" "-")
  :group 'python)

(reformatter-define python-sort-imports
  :program "ruff"
  :args '("--fix" "--select" "I001" "-")
  :group 'python)

;; we use elpy just to have `elpy-test'
(use-package elpy
  :ensure t
  :defer t
  :custom
  ;; always print stdout when running tests with pytest
  (elpy-test-pytest-runner-command '("pytest" "-s" "-vv"))
  (elpy-shell-echo-input nil))

(setq python-shell-completion-native-enable nil)

(defun my/ensure-elpy-is-loaded ()
  "Check if `elpy' is loaded otherwise load it."
  (unless (featurep 'elpy)
    (require 'elpy)))

(with-eval-after-load 'python
  (evil-define-key nil python-mode-map (kbd "<leader>d") 'my/goto-definition-dumb-jump-fallback)
  (define-key python-mode-map (kbd "C-c C-f") 'python-ruff-format-buffer)
  (define-key python-mode-map (kbd "C-c C-t") 'elpy-test)
  (define-key python-mode-map (kbd "C-c C-i") 'python-sort-imports-region)
  (add-hook 'python-mode-hook #'flymake-ruff-load)
  (add-hook 'python-mode-hook 'my/ensure-elpy-is-loaded))

Show a list of the available django commands and run the selected one using a compilation buffer.

(defun my/run-django-command ()
  "Run a django command."
  (interactive)
  (let* ((python-bin (concat (getenv "VIRTUAL_ENV") "/bin/python"))
         (manage-py-file (concat (my/project-root) "manage.py"))
         (default-directory (my/project-root))
         (raw-help (shell-command-to-string (concat python-bin " " manage-py-file " help")))
         (splited-lines (split-string raw-help "\n"))
         (options (seq-filter #'(lambda (line) (cl-search "    " line)) splited-lines))
         (selection (completing-read "Pick django command: " (mapcar 'string-trim options)))
         (command (concat python-bin " " manage-py-file " " selection)))
    (compile command)))

19.4 Erlang

Clone erlang source code into ~/Code/erlang/src/

git clone https://github.com/erlang/otp.git ~/Code/erlang/src/
(use-package erlang
  :ensure t
  :defer t
  :if (executable-find "erl")
  :config
  (setq erlang-root-dir (expand-file-name "~/Code/erlang/src"))
  (require 'erlang-start))

19.5 Elixir

To have support for LSP we need to compile elixir-ls and setup the generated release into eglot-server-programs

mix elixir_ls.release

In case project mix.exs is not in root folder we need to tell elixir_ls where is the correct location using a .dir-locals.el file.

((elixir-mode . ((eglot-workspace-configuration . ((:elixirLS . (:projectDir "mix.exs dir/")))))))
(reformatter-define elixir-format
  :program "mix"
  :args '("format" "-")
  :group 'elixir)

(defun my/elixir-format-buffer ()
  "Format elixir buffers using eglot when is active otherwise use reformatter function."
  (interactive)
  ;; eglot formatter is preferred because it will use project .formatter.exs file
  ;; regular formatter generated by reformatted will ignore that file
  (if (and (featurep 'eglot) (eglot-managed-p))
      (eglot-format-buffer)
    (elixir-format-buffer)))

(use-package elixir-mode
  :ensure t
  :bind (:map elixir-mode-map
              ("C-c C-t" . 'my/mix-run-test-at-point)
              ("C-c C-f" . 'my/elixir-format-buffer))

  :config
  (evil-define-key nil elixir-mode-map (kbd "<leader>d") 'my/goto-definition-dumb-jump-fallback))

heex templates support

(use-package heex-ts-mode
  :ensure t
  :hook (heex-ts-mode . emmet-mode))

Custom functions to run elixir tests.

elixir-extra-test-env can be set up on .dir-locals.el

(defun my/mix-run-test (&optional scope)
  "Run elixir test for the given SCOPE."
  (interactive)
  (let* ((current-file (buffer-file-name))
         (current-line (line-number-at-pos))
         (mix-file (expand-file-name (concat (locate-dominating-file (buffer-file-name) "mix.exs") "mix.exs")))
         (default-directory (file-name-directory mix-file))
         (extra-env (if (boundp 'elixir-extra-test-env) elixir-extra-test-env ""))
         (mix-env (concat "MIX_ENV=test " extra-env)))

    (cond
     ((string-equal scope "file")
      (compile (format "%s mix test %s" mix-env current-file)))

     ((string-equal scope "at-point")
      (compile (format "%s mix test %s:%s" mix-env current-file current-line)))

     (t
      (compile (format "%s mix test" mix-env))))))


(defun my/mix-run-test-file ()
  "Run mix test over the current file."
  (interactive)
  (my/mix-run-test "file"))

(defun my/mix-run-test-at-point ()
  "Run mix test at point."
  (interactive)
  (my/mix-run-test "at-point"))

(defun my/mix-run-test-all ()
  "Run mix test at point."
  (interactive)
  (my/mix-run-test))

19.6 LFE

(use-package lfe-mode
  :ensure t
  :if (executable-find "lfe")
  :bind (:map lfe-mode-map
              ("C-c C-c" . lfe-eval-buffer))
  :init
  (defun lfe-eval-buffer ()
    "Send current buffer to inferior LFE process."
    (interactive)
    (if (eq (get-buffer-window "*inferior-lfe*") nil)
        (run-lfe nil))
    (lfe-eval-region (point-min) (point-max) nil)))

19.7 Elm

Install Elm

npm -g install elm elm-format elm-oracle
(use-package elm-mode
  :ensure t
  :if (executable-find "elm")
  :bind (:map elm-mode-map
              ("C-c C-d" . elm-oracle-doc-at-point))
  :config
  (add-hook 'elm-mode-hook #'elm-oracle-setup-completion)
  (add-to-list 'company-backends 'company-elm))

19.8 Haskell

Install haskell binaries hlint and hindent and make sure ~/.local/bin/ is loaded in PATH.

stack install hlint
stack install hindent
(reformatter-define haskell-format
  :program "hindent"
  :group 'haskell)

(use-package haskell-mode
  :ensure t
  :bind (:map haskell-mode-map
              ("C-c C-f" . haskell-format-buffer)
              ("C-c C-l" . haskell-process-load-file)))

(defun my/run-hlint ()
  "Run  hlint over the current project."
  (interactive)
  (let ((default-directory (my/project-root)))
    (compile "hlint .")))

(defun my/run-hlint-buffer ()
  "Run  hlint over the current buffer."
  (interactive)
  (let* ((current-file (buffer-file-name))
         (default-directory (my/project-root)))
    (compile (concat "hlint " current-file))))

19.9 Lua

(use-package lua-mode
  :ensure t
  :bind (:map lua-mode-map
              ("C-c C-b" . compile)
              ("C-c C-f" . lua-format-buffer)))

Define formatter using StyLua

(reformatter-define lua-format
  :program "stylua"
  :args '("-")
  :group 'lua)

19.10 Javascript

We use default js-mode because it has been improved in emacs 27.

19.10.1 Formattter

We need to use --stdin-filepath a.js to tell prettier to use js parser.

(reformatter-define js-format
  :program "npx"
  :args '("prettier" "--stdin-filepath" "a.js"))

(with-eval-after-load 'js
  (evil-define-key nil js-mode-map (kbd "<leader>d") 'my/goto-definition-dumb-jump-fallback)
  (define-key js-mode-map (kbd "C-c C-f") 'js-format-buffer))

19.11 Typescript

(use-package typescript-mode
  :ensure t
  :defer t
  :mode "\\.tsx?\\'"
  :config
  (evil-define-key nil typescript-mode-map (kbd "<leader>d") 'my/goto-definition-dumb-jump-fallback))

19.12 Rust

Install rust analyzer, this should be installed when rustup-init is executed but in case is not we can execute:

rustup component add rust-analyzer

Install rust source code, it is required by rust-analyzer, in case it's not installed automatically

rustup component add rust-src
(use-package rust-mode
  :ensure t
  :if (executable-find "rustc")
  :config
  (evil-define-key nil rust-mode-map (kbd "<leader>d") 'my/goto-definition-dumb-jump-fallback))
(defun my/rust-run-file ()
  "Compile and rust current file."
  (interactive)
  (unless (buffer-file-name)
    (user-error "Save file before"))
  (let* ((path (buffer-file-name))
         (default-directory (file-name-directory path))
         (filename (buffer-name))
         (command (format "rustc %s && ./%s" filename (string-replace ".rs" "" filename))))
    (compile command)))

19.13 Golang

Install dependencies: goimports

go install golang.org/x/tools/cmd/goimports@latest

Install gopls to have LSP support using eglot

brew install gopls
(use-package go-mode
  :ensure t
  :if (executable-find "go")
  :bind (:map go-mode-map
              ("C-c C-t" . go-test-current-file)
              ("C-c C-c" . go-run)
              ("C-c C-f" . gofmt))
  :hook (go-mode . eglot-ensure)
  :config
  ;; by default tab width is 8, that's too much space so we define 4
  ;; only for go buffers
  (add-hook 'go-mode-hook #'(lambda ()
                              (setq-local tab-width 4)))
  (setq gofmt-command "goimports")
  (evil-define-key nil go-mode-map (kbd "<leader>d") 'my/goto-definition-dumb-jump-fallback))

(use-package go-playground
  :ensure t
  :if (executable-find "go")
  :after go-mode
  :config
  (setq go-playground-basedir (expand-file-name "~/Code/golang/playgrounds")))

19.14 Common lisp

(defconst inferior-lisp-program (executable-find "sbcl"))

(use-package sly
  :ensure t
  :defer t)

19.15 Clojure

(defun my/clj-format-code ()
  "Format clojure code using cider commands."
  (interactive)
  (if (region-active-p)
      (cider-format-region (region-beginning) (region-end))
    (cider-format-buffer)))

(defun my/cider-repl-reset ()
  "Call (reset) in the active repl and return to the position where was called."
  (interactive)
  (save-window-excursion
    (cider-insert-in-repl "(reset)" t)))

(use-package cider
  :ensure t
  :bind (:map cider-mode-map
              ("C-c C-f" . my/clj-format-code)
              ("C-c C-r" . my/cider-repl-reset)))

(evil-define-key nil clojure-mode-map (kbd "<leader>d") 'cider-find-var)

(with-eval-after-load 'evil
  (evil-set-initial-state 'cider-stacktrace-mode 'emacs))
(use-package clj-refactor
  :ensure t
  :after cider
  :bind (:map clojure-mode-map
              ("C-c C-a" . cljr-add-project-dependency))
  :hook (clojure . clj-refactor))

19.16 Emacs lisp

Enable go to definition with \ d keybinding

(evil-define-key nil emacs-lisp-mode-map (kbd "<leader>d") 'xref-find-definitions)
(evil-define-key nil lisp-interaction-mode-map (kbd "<leader>d") 'xref-find-definitions)

Disable indentation with tabs for emacs-lisp-mode

(defun my/emacs-lisp-hook-setup ()
  (setq indent-tabs-mode nil))

(add-hook 'emacs-lisp-mode-hook 'my/emacs-lisp-hook-setup)

Enable flymake

(add-hook 'emacs-lisp-mode-hook 'flymake-mode-on)

package-lint, used for packages development

(use-package package-lint
  :ensure t
  :defer t)

19.17 OCaml

(use-package tuareg
  :ensure t
  :defer t)

(use-package merlin
  :ensure t
  :hook ((tuareg-mode caml-mode) . merlin-mode))

(use-package merlin-eldoc
  :ensure t
  :hook ((reason-mode tuareg-mode caml-mode) . merlin-eldoc-setup))

19.18 Dart

(reformatter-define dart-format
  :program "dart"
  :args '("format")
  :group 'dart)

(defun my/dart-run-file ()
  "Execute the code of the current file."
  (interactive)
  (compile (format "dart %s" (buffer-file-name))))

(use-package dart-mode
  :ensure t
  :if (or (executable-find "dart") (executable-find "flutter"))
  :bind (:map dart-mode-map
              ("C-c C-f" . dart-format-buffer)
              ("C-c C-c" . my/dart-run-file))
  :config
  (evil-define-key nil dart-mode-map (kbd "<leader>d") 'xref-find-definitions))

19.18.1 Flutter

(defun my/flutter-goto-logs-buffer()
  "Go to buffer logs buffer."
  (interactive)
  (let ((buffer (get-buffer flutter-buffer-name)))
    (unless buffer
      (user-error "flutter is not running."))
    (switch-to-buffer buffer)
    (goto-line (point-max))))

(use-package flutter
  :ensure t
  :after dart-mode
  :bind (:map dart-mode-map
              ("C-c C-r" . #'flutter-run-or-hot-reload)
              ("C-c C-l" . #'my/flutter-goto-logs-buffer))
  :hook (dart-mode . flutter-test-mode)
  :custom
  ;; sdk path will be the parent-parent directory of flutter cli
  (flutter-sdk-path (directory-file-name
                     (file-name-directory
                      (directory-file-name
                       (file-name-directory (executable-find "flutter")))))))

19.19 F-sharp

(use-package fsharp-mode
  :ensure t
  :defer t
  :if (executable-find "dotnet")
  :config
  (evil-define-key nil fsharp-mode-map (kbd "<leader>d") 'fsharp-ac/gotodefn-at-point))

20 Writing

Custom functions to speed up writing process

20.1 Hugo

Insert org-link image using clipboard value, if the current file is blog/demo.org it will place the resulting image into static/images/blog/demo/image.png.

(defun my/hugo-insert-image-from-clipboard ()
  "Use clipoard image and put it in a generated images folder for the current file."
  (interactive)
  (let* ((absolute-path (buffer-file-name))
         (splitted (reverse (split-string absolute-path "/")))
         (filename (replace-regexp-in-string ".org" "" (car splitted)))
         (dir (nth 1 splitted))
         (base-image-path (concat (my/project-root) "static/images"))
         (result-image-dir (format "%s/%s/%s" base-image-path dir filename))
         (result-image-name (read-string "Filename: " "image.png"))
         (full-path-result-image (format "%s/%s" result-image-dir result-image-name)))

    (shell-command (format "mkdir -p %s" result-image-dir))
    (shell-command (format "pngpaste %s" full-path-result-image))
    (insert (format "[[file:%s]]" (car (cdr (split-string full-path-result-image "static")))))))

Insert docsy link

(defun my/docsy-insert-ref ()
  "Insert a link using docsy ref helper."
  (interactive)
  (let* ((filename (read-file-name "Select file: " (my/project-root)))
         (prefix-to-remove (concat (my/project-root) "content/"))
         (relative-path (string-replace prefix-to-remove "" filename)))
    (insert (format "{{< ref \"%s\" >}}" relative-path))))

21 Custom functions

Simple bookmarks management

(defvar my/bookmarks (make-hash-table :test 'equal))

(defun my/bookmark-switch ()
  "Switch to selected bookmark."
  (interactive)
  (let* ((key (my/project-name))
         (items (gethash key my/bookmarks (make-hash-table :test 'equal)))
         (options (hash-table-keys items))
         (selected (completing-read "Pick buffer: " options))
         (selected-buffer (gethash selected items)))
    (when selected-buffer

      (switch-to-buffer selected-buffer))))

(defun my/add-bookmark ()
  "Add current buffer to bookmark list."
  (interactive)
  (let* ((project-key (my/project-name))
         (buffer-key (buffer-name))
         (items (gethash project-key my/bookmarks (make-hash-table :test 'equal))))
    (puthash buffer-key (current-buffer) items)
    (puthash project-key items my/bookmarks)
    (message "%s added to bookmarks" buffer-key)))

Manage window configurations, allows to save a "snapshot" of the current windows configuration. Also allows to restore a saved "snapshot".

(defvar my/window-snapshots '())

(defun my/save-window-snapshot ()
  "Save the current window configuration into `window-snapshots` alist."
  (interactive)
  (let ((key (read-string "Enter a name for the snapshot: ")))
    (setf (alist-get key my/window-snapshots) (current-window-configuration))
    (message "%s window snapshot saved!" key)))

(defun my/get-window-snapshot (key)
  "Given a KEY return the saved value in `window-snapshots` alist."
  (let ((value (assoc key my/window-snapshots)))
    (cdr value)))

(defun my/restore-window-snapshot ()
  "Restore a window snapshot from the window-snapshots alist."
  (interactive)
  (let* ((snapshot-name (completing-read "Choose snapshot: " (mapcar #'car my/window-snapshots)))
         (snapshot (my/get-window-snapshot snapshot-name)))
    (if snapshot
        (set-window-configuration snapshot)
      (message "Snapshot %s not found" snapshot-name))))

Manipulate frame font height.

(defun my/change-font-height (delta)
  "Use DELTA to increase/decrease the frame font height."
  (let* ((current-height (face-attribute 'default :height))
         (new-height (+ current-height delta)))
    (set-face-attribute 'default nil :height new-height)))

(defun my/decrease-font-height ()
  "Decrease font height by 10."
  (interactive)
  (my/change-font-height -10))

(defun my/increase-font-height ()
  "Increase font height by 10."
  (interactive)
  (my/change-font-height +10))
(defun my/find-file-in-project ()
  "Custom find file function."
  (interactive)
  (if (my/project-p)
      (project-find-file)
    (call-interactively #'find-file)))

(defun my/fold-buffer-when-is-too-big (max-lines)
  "Fold buffer is max lines if grater than as MAX-LINES."
  (if (> (count-lines (point-min) (point-max)) max-lines)
      (hs-hide-all)))

(defun my/toggle-maximize ()
  "Toggle maximization of current window."
  (interactive)
  (let ((register ?w))
    (if (eq (get-register register) nil)
        (progn
          (set-register register (current-window-configuration))
          (delete-other-windows))
      (progn
        (set-window-configuration (get-register register))
        (set-register register nil)))))

(defun my/venv-workon (name)
  "Active virtualenv NAME only is not setup yet."
  (unless pyvenv-virtual-env
    (pyvenv-workon name)))

(defun my/config-file ()
  "Open config file."
  (interactive)
  (find-file (expand-file-name "~/.emacs.d/bootstrap.org")))

(defun my/toggle-spanish-characters ()
  "Enable/disable alt key to allow insert spanish characters."
  (interactive)
  (if (eq ns-alternate-modifier 'meta)
      (setq ns-alternate-modifier nil)
    (setq ns-alternate-modifier 'meta)))

(defun my/change-font-size()
  "Change frame font size."
  (interactive)
  (let* ((size (read-number "New size: ")))
    (set-face-attribute 'default nil :font (format "%s %d" my/font-family size))))

Function to extract clocks from org buffer and filter them by month

(defun my/collect-clocks ()
  "Collect all the clocks of current buffer."
  (org-element-map (org-element-parse-buffer) 'clock #'(lambda (clock) clock)))

(defun my/filter-clocks-by-month (clocks month)
  "Filter CLOCKS using MONTH value."
  (seq-filter #'(lambda (clock)
                  (eq (org-element-property :month-end (org-element-property :value clock)) month)) clocks))

(defun my/org-filter-clocks-report ()
  "Create a buffer with the tasks filtered by month."
  (interactive)
  (let* ((month (read-number "Insert month: "))
         (clocks (my/collect-clocks))
         (filtered-clocks (my/filter-clocks-by-month clocks month))
         (buffer (get-buffer-create "*clocks report*")))
    (switch-to-buffer buffer)
    (org-mode)
    (insert "* Report\n")
    (seq-map #'(lambda (clock)
                 (insert (format "CLOCK: %s\n" (org-element-property :raw-value (org-element-property :value clock))))) filtered-clocks)
    (org-clock-display)))

Copy absolute and relative path to clipboard

(defun my/copy-abs-path ()
  "Copy absolute path of the buffer to clipboard"
  (interactive)
  (if buffer-file-name
      (progn
        (kill-new buffer-file-name)
        (message (format "%s copied to clipboard" buffer-file-name)))
    (message "File not saved yet")))

(defun my/copy-relative-path ()
  "Copy relative path of the buffer to clipboard"
  (interactive)
  (if (and (my/project-p) buffer-file-name)
      (let ((path (file-relative-name buffer-file-name (my/project-root))))
        (kill-new path)
        (message (format "%s copied to clipboard" path)))
    (message "File not saved yet or not inside project")))

Create a temp file with the current buffer content and render it with eww.

(defun my/preview-buffer-in-eww ()
  "Preview buffer content in EWW."
  (interactive)
  (let* ((temp-file (make-temp-name (temporary-file-directory)))
         (path (concat temp-file ".html")))
    (write-file path)
    (kill-buffer)
    (eww-open-file path)))

(defun my/preview-buffer-in-xwidget-browser ()
  "Preview buffer content in EWW."
  (interactive)
  (let* ((temp-file (make-temp-name (temporary-file-directory)))
         (path (concat temp-file ".html")))
    (write-file path)
    (kill-buffer)
    (xwidget-webkit-browse-url (format "file://%s" path))))

Resize window: allow create a "resize mode" and use hjkl to increase/decrease width/height of the current window

(defun my/resize-window ()
  "Resize window using j k h l keys."
  (interactive)
  (let ((keys-map '((?h . evil-window-decrease-width)
                    (?j . evil-window-decrease-height)
                    (?k . evil-window-increase-height)
                    (?l . evil-window-increase-width)))
        (overlay (make-overlay (point-min) (point-max) (window-buffer))))
    (let ((is-reading t))
      (overlay-put overlay 'face '((t (:foreground "gray40"))))
      (while is-reading
        (let ((action (alist-get (read-key) keys-map)))
          (if action
              (apply action '(1))
            (setq is-reading nil)
            (delete-overlay overlay)))))))

Kill the current buffer and delete the related file

(defun my/delete-close-file ()
  "Delete the current file and kill its buffer."
  (interactive)
  (when buffer-file-name
    (delete-file buffer-file-name)
    (kill-buffer)))

Copy json text from clipboard in a new buffer and format it

(defun my/copy-and-format-json-from-clipboard ()
  "Copy content from clipboard and format it in a new buffer."
  (interactive)
  (let ((buffer (generate-new-buffer "tmp.json")))
    (with-current-buffer buffer
      (yank)
      (jsonian-mode)
      (json-pretty-print-buffer))
    (set-window-buffer nil buffer)))

21.1 MacOS

Functions to open Finder using current file or current project.

(defun my/open-finder-at (path)
  "Open Finder app with the given PATH."
  (let* ((finder (executable-find "open"))
         (command (format "%s %s" finder path)))
    (shell-command command)))

(defun my/open-project-in-finder ()
  "Open current project in Finder app."
  (interactive)
  (if (my/project-p)
      (my/open-finder-at (my/project-root))
    (message "There is no active project.")))

(defun my/open-current-file-in-finder ()
  "Open current file in Finder."
  (interactive)
  (let ((file (buffer-file-name)))
    (if file
        (my/open-finder-at (file-name-directory file))
      (message "Buffer has not been saved yet!"))))

Open current file with an macOS app. Installed macOS apps will be listed using completing-read

(defun my/macos-open-file-with ()
  "Open current file with and macOS installed app."
  (interactive)
  (let* ((apps-list (directory-files "/Applications" nil "\\.app$"))
         (selected-app (completing-read "Choose an application: " apps-list)))
    (shell-command (format "open %s -a '%s'" (buffer-file-name) selected-app))))

Open the current file with macOS open command. This will open the file with the default app configured for the type of file.

(defun my/macos-open-current-file ()
  (interactive)
  (shell-command (format "open %s" (buffer-file-name))))

Save image from clipboard to path.

(defun my/save-image-from-clipboard ()
  "Save image from clipboard to the given path."
  (interactive)
  (unless (executable-find "pngpaste")
    (user-error "Install pngpaste to continue"))
  (let* ((path (read-file-name ""))
         (command (format "pngpaste %s" path)))
    (shell-command command)
    (kill-new path)))

Author: Erick Navarro

Created: 2024-04-10 Wed 11:36

Validate