Working inside emacs

Table of Contents

1. Emacs tab

How I manually insert tabs inside emacs ?

Emacs prevents inserting tabs, I think it's because the key is bound to complete when you set the variable indent-tabs-mode to nil.

Why would you set this variable to nil ?

This variable permit to complete inside minibuffer or the buffer when you type tab, it's pretty handy as its the default that people are used to from other editors.

How to then manually insert your tabs ?

You can use the key sequence C-q <tab> to insert a tab, but electric-mode will cause you some ugly indentation for the next lines.

The second solution is to use the interactive function tab-to-tab-stop. It permit to go to the next tab-stop.

I bound this function to the key C-<tab> so that I can quickly insert tabs wherever I want : (global-set-key (kbd "C-<tab>") 'tab-to-tab-stop)

2. Merge my linux and mac configuration

How to execute a elisp bloc depending on whether you're on mac or linux ?

The variable system-type is set to your current system.

darwin for macos and gnu/linux for linux.

You just have to use a condition depending on what you want to execute on the current system.

Example :

(cond
 ((eq system-type 'darwin)  ;; macOS
  (setq read-process-output-max (* 64 1024)))  ;; 64KB
 ((eq system-type 'gnu/linux)  ;; Linux
  (setq read-process-output-max (* 1024 1024)))  ;; 1MB
 )

Do not forget variable like user-emacs-directory that helps you and diminishes the use of the conditional statements.

3. Set transparent background

How to set transparent background in emacs ?

To set transparent background in emacs, there are two methods, only one works for each systems (in my experience).

On linux, what worked for me :

(set-frame-parameter nil 'alpha-background 60)
(add-to-list 'default-frame-alist '(alpha-background . 60))

On mac, what worked :

(set-frame-parameter (selected-frame) 'alpha '(92 . 50))
(add-to-list 'default-frame-alist '(alpha . (92 . 50))))

4. Set up emacs to load pkgs installed from nix

How to load the packages that you installed from nix inside emacs ?

We're not talking about binaries, but emacs packages. Such packages should not make modification to any path, unless you want an impure environment.

4.1. First solution [deprecated]

The first solution, which is the simplest, is to just open a devshell, in which you've installed and initialize the emacs packages. An example for julia environment

{
  description = "Environment to code in Julia";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let 
        pkgs = import nixpkgs { inherit system; };
      in {
        devShell = pkgs.mkShell {
          nativeBuildInputs = with pkgs; [
            julia-bin
            emacsPackages.julia-ts-mode
            bc
          ];

          shellHook = ''
            julia -e 'import Pkg; Pkg.add("DebugAdapter")' \
            julia -e 'import Pkg; Pkg.add("LanguageServer")' \
            emacs --batch --eval "(progn (package-initialize))" \
            echo "My julia environment for coding"
          '';
        };
      }
    );
}

The packages should be present and loaded after your run `emacs &` in the dev shell.

4.2. Second solution

Why bother with this second solution ?

While building the dev environment using nix, the first solution demands to quit emacs, make your changes and reload emacs to check if all is correctly setup. That was such a pain that I decided to find a solution. (I hate doing the same things in repitition)

The second solution involves using packages like envrc (the one I use) or direnv (an alternative).

I use envrc becauses it loaded the variables from direnv at a buffer level instead of inserting them to emacs global state.

My configuration looks like :

(use-package envrc
    :config
    (envrc-global-mode))

For this to work, you should have the direnv binary, an .envrc file inside your root dir, and had allowed direnv to automatically load your shell environment. Details here : nix direnv workflow and direnv documentation

The fact is, with this method, the binary will be inside the path of emacs and you'll be able to use them, but not the emacsPackages, because the load-path will not gets populate with those packages root directories.

The workaround I found for this is to add those directories to the load path with some elisp code. Here is my custom function that does that :

(defun smv/add-nix-pkg-to-lpath (PKG_ENV)
    "Load the PKG_ENV directory to the load path of current emacs session
    it permits to then require the package"

    (let ((pkg-nix-path (getenv PKG_ENV)))

      (unless pkg-nix-path
        (user-error "Environment variable '%s' is not set" PKG_ENV))

      (let ((pkg-suffix "/share/emacs/site-lisp/elpa/"))

        (string-match "-emacs-\\([^/]+\\)" pkg-nix-path)

        (let* ((pkg-full-path (match-string 1 pkg-nix-path))
               (path-to-add (concat pkg-nix-path pkg-suffix pkg-full-path)))
          (unless (member path-to-add load-path)
            (add-to-list 'load-path path-to-add))))))

Depending on some environment variable it add the "/share/emacs/site-elisp/elpa/<package-name>" directory of this environment variable to the load path.

The required environment variable here is the nix-path of the package you want to load.

How I use it ?

{
  ...
        devShell = pkgs.mkShell {
          nativeBuildInputs = with pkgs; [
            julia-bin
            emacsPackages.julia-ts-mode
            bc
          ];

          shellHook = ''
            export EMACS_JULIATS = "${pkgs.emacsPackages.julia-ts-mode}"
            echo "My julia environment for coding"
          '';
        };

   ...
}

Look at the shellHook bloc, I've defined a shell environment variable that will be buffer available when envrc will run direnv.

Then you can call my custom function using this variable as an argument. For example :

(smv/add-nix-pkg-to-lpath EMACS_JULIATS)

You can add such call to your .dir-locals.el file inside the root directory of your project and that's it.

4.3. Third solution

The last solution involves devbox, it's a wrapper around nix that you could use without knowing the nix language. It's pretty cool if you only use nix as an dev environment manager.

After the initialization of devbox in your project, you should generate the direnv file : generate direnv

The approach here will be to take advantage of the environment variable EMACSLOADPATH. This variable is used to build the load-path at emacs' start.

I wrote this script that you could add to your devbox inithook, such that it's ran when you enter the shell.

EMACS_LOAD_PATHS=''
for pkg in $(printenv buildInputs | tr ' ' '\\n' | grep -E 'emacs.*-mode');
do
    PKG_NAME=$(basename $pkg | awk -F- '{ printf "%s-%s", $3, $4 }')
    PKG_VERSION=$(devbox info emacsPackages.$PKG_NAME 2>/dev/null | awk '{print $2}' || echo '')
    if [ -n "$PKG_VERSION" ]; then
        EMACS_LOAD_PATHS="$pkg/share/emacs/site-lisp/elpa/$PKG_NAME-$PKG_VERSION:$EMACS_LOAD_PATHS"
    fi
done
export EMACSLOADPATH="$EMACS_LOAD_PATHS$EMACSLOADPATH"
echo "✓ Emacs packages loaded"

Or a more reliable one, as it doesn't depends on finding the version and concatenating /share/emacs/site-lisp/elpa

EMACS_LOAD_PATHS='',
for pkg in $(printenv buildInputs | tr ' ' '\\n' | grep -E 'emacs.*-mode'); do,
  LONGEST_PATH=$(find $pkg -name '*.el' -type f -exec dirname {} \\; 2>/dev/null | awk '{ print length, $0 }' | sort -rn | head -1 | cut -d' ' -f2-),
  if [ -n $LONGEST_PATH ]; then,
    EMACS_LOAD_PATHS=$LONGEST_PATH:$EMACS_LOAD_PATHS,
  fi,
done,
export EMACSLOADPATH=$EMACS_LOAD_PATHS$EMACSLOADPATH,
echo "✓ Emacs packages loaded"

To be able to do that in the most efficient way, what I've done, is just to create a new snippet that contains this code snippet.

This command will go through the packages that you've added and add the emacs' ones to the environment variable EMACSLOADPATH.

At this point of time running emacs in the devshell created by devbox should permit to get the emacs packages. We should now make it such that the same thing is done even in the current instance of emacs.

At the time of writing this I use direnv-mode now. With direnv-mode, I can run the interactive command direnv-update-environment and that will run direnv and inject the variables into the emacs' global variables list. So the thing to do is just to advice after this command. This will permit to add the paths to the load-path. I've written this code snippet that you can add to your configuration :

(with-eval-after-load 'direnv
  (advice-add 'direnv-update-environment :after
              (lambda (&rest _)
                (when (getenv "EMACSLOADPATH")
                  (let ((paths (split-string (getenv "EMACSLOADPATH") ":")))
                    (dolist (path (reverse paths))
                      (add-to-list 'load-path path)))))))

And that is it. Running direnv-update-environment will also add the paths in EMACSLOADPATH to the current load-path of emacs.

5. How to code per file

This notes that I take requires me to modify my index.html file each time. I could've use a command to trigger the modification automatically. But the solution that came up first into my mind was to create a snippet that will only be available in that file. This way I can open my file add my new entry and that's it.

To execute some code on a directory level in emacs you could use a file called .dir-locals.el, to execute code in a per-file basis you use a ;; -*- eval: <code> ; -*- on top of that file.

eval means evaluate, you could set others things, such as the mode, with mode like mode: lisp for example.

So here's what I wrote on top on my file, it uses the auto-yasnippet's package variable aya-current :

;; -*- eval: (progn (setq aya-current "<li><a href=\"${1:org-file-name}.html\">${2:title}</a></li>")); -*-

6. How to scroll inside vterm

vterm has multiple mode, it's not like eshell that permits you to scroll without restriction inside the terminal.

In vterm, you should switch to the copy-mode to be able to scroll inside the terminal.

The default binding for that is : C-c C-t

7. How to find then filter down and replace in emacs

You can find patterns in folders in Emacs using `find-grep`, `find-grep-dired` (which will only show the file names inside a dired buffer), `rgrep`, and similar commands. All of these will open a grep buffer that utilizes `compilation-mode` bindings.

How to filter the results and then perform replacements ?

You can make the grep buffer read-only using `C-x C-q` (which runs `toggle-read-only`) and then use commands like `keep-lines` or `flush-lines` to filter the results. Alternatively, you can manually remove the lines you don't want from the list.

Once you have your desired selection, switch to `wgrep` mode (`wgrep-change-to-wgrep-mode`) to perform operations on the occurrences.

8. How to search for patterns in files in emacs using consult

Consult is the search package that you use.

How to look for some pattern into some precise list of files You can use the prefix command with consult-grep and consult-ripgrep. Then you can list the different files you want to look into, comma-separated, and here it is.

You can also use directly filtering patterns, as those commands support the command line parameters their utility uses. For example you can search in all files ending with .org by typing in the consult-ripgrep prompt :

#foo bar --glob=*.org or #foo bar -g *.org: Search into org files 
#foo bar --glob=!*.org or #foo bar -g !*.org: Search into non org files
#.* -F: Treat input as fixed string, and not as regular expression.
#foo bar -s: Treat input as case sensitive (-s), insensitive (-i), or smart case (-S).
#foo bar --context=2 or #foo bar -C2: Include two lines of context.
#foo bar --hidden or #foo bar -.: Search hidden files.

9. Setting compilation environment variable

When you're coding inside emacs you might want to run some commands using the project-compile command, which is really powerfull. Sometimes you might want to set some compilation environment that is only needed during this command evaluation process, you don't need that variable outside. For example you might want to add some path to the PYTHONPATH for your project to correctly run.

To set those kind of commands, pay attention to the fact that using the "$PYTHONPATH" formula will not work. Prefer setting the environment by retrieving the variable value then add the new string to it.

What I mean is :

Prefer : (setq compilation-environment (list (concat "PYTHONPATH=" (getenv "PYTHONPATH") ":" (expand-file-name "./src")))) or (setq compilation-environment (list (concat "PYTHONPATH=" (getenv "PYTHONPATH") ":./src")))

instead of : (setq compilation-environment '("PYTHONPATH=$PYTHONPATH:./src"))

10. Working with macros

10.1. Executing macros with variations

While recording macros, you can use the command C-x q to stop at some point. This command when you type it in the macro will not do anything but when you will play back the macro emacs will ask you wheter you want to continue the macro or not. To this question you can answer C-r which will make emacs enter in a recursive edit mode where you can do things outside of the macro, like typing a particular piece of text. After you've finished, you can type C-M-c to leave that recursive state and you will be prompted whether or not you want the macro to continue its execution.

The documentation for what I've explained

10.2. Editing macros interactively

You can edit macros interactively, meaning while playing it. The shortcut for that is : C-x C-k SPC

stepwise editing macros

10.3. Switching between macros

As the previous point said, a macro ring exists. You are able to jump between one macro you've recorded or another without having to save them before and bind them. To do that : C-x C-k C-n > ~kmacro-cycle-ring-next~ =C-x C-k C-p => kmacro-cycle-ring-previous

10.4. Reorganizing macros

You can reorganize the macros' kill ring. Start by listing them with : list-keyboard-macros and here it's you can modify them

10.5. Building a new ts mode

I'm taking inspiration on the go-ts-mode, so I'll use that as my reference to look for information and understand each parts of the ~500 lines of code project.

  • An Emacs Lisp syntax table defines how Emacs interprets characters for various syntactic purposes like highlighting, navigation (e.g., `forward-word`), and commenting. It assigns a "syntax class" and other properties to each character.

Created: 2026-04-20 Mon 22:53