Categories: geek » emacs » org

View topic page - RSS - Atom - Subscribe via email

Carnaval d'Emacs d'août 2026 : la gestion d'information et les graphes de connaissances

| emacs, french, org

Cet article est inspiré par le Carnaval d'Emacs sur la recherche de connaissances. Merci à Charlie Holland pour son accueil ! C'est une bonne occasion de réfléchir à la gestion de mes notes, qui est un de mes intérêts forts.

Saisir : Je souhaite saisir et publier ce que j'apprends aussitôt que possible parce que ma mémoire n'est pas fiable. Je préfère le format texte brut car il est le plus consultable et le plus archivable à long terme. Je suis loin de mon ordinateur la plupart du temps, donc j'utilise Orgzly Revived sur mon téléphone pour saisir des notes courtes dans ma boîte de réception inbox.org ou mon fichier de brouillons posts.org. Une fois que je suis sur mon ordinateur, je développe mes notes en utilisant Org Mode sur Emacs. J'utilise org-refile pour déplacer des notes vers d'autres fichiers comme organizer.org. J'utilise quelques grands fichiers Org. Pour les tâches répétitives comme mes flux de travail, j'y ajoute des détails autant que possible.

Chercher : Pour chercher mes notes publiques, j'utilise souvent Google. Les notes publiques me permettent de les récupérer facilement. Si je publie mes notes, d'autres personnes peuvent en profiter et les enrichir. Forcément, j'ai aussi des notes personnelles. J'utilise org-refile pour chercher par titres ou consult-ripgrep pour naviguer dans mes notes privées. J'utilise aussi consult-line et isearch si je veux chercher par mots dans le corps. Ils reposent sur des comparaisons exactes, mais si j'essaie des mots similaires, je ne peux pas trouver ce que je cherche. C'est la raison pour laquelle je m'intéresse au paquet p-search et aux plongements de phrases (embeddings) pour la recherche approximative, mais je n'ai pas encore mis en place un bon flux de travail. Je rêve d'un système pour suggérer automatiquement des liens vers mes autres articles, ma configuration et mes notes privées, ce qui peut me rafraîchir la mémoire sur des choses oubliées. Une vraie évaluation doit attendre d'avoir plus de temps libre.

Naviguer : J'utilise C-u org-refile pour naviguer dans mes sous-titres n'importe où dans mes fichiers org-refile-targets. Je relis aussi ma boîte de réception et mes brouillons de temps en temps. J'ai une fonction sacha-blog-edit-org qui ouvre le code source Org à partir d'un lien, même s'il est dans mon posts.org ou seulement dans la copie publiée.

Lier : Beaucoup de mes idées sont inspirées par des articles d'autres personnes, donc quand je trouve un article intéressant sur mon téléphone, je l'envoie à Orgzly Revived pour l'ajouter à ma boîte de réception. Ça me permet d'inclure le lien ou les liens dans l'article une fois que je réussis finalement à l'écrire et à le publier.

J'ai une petite fonction sacha-org-contacts-suggest-mentions qui m'aide à notifier l'auteur de l'article précédent et peut-être d'autres personnes qui sont potentiellement intéressées en utilisant mon fichier people.org, où j'ai spécifié des expressions régulières à comparer au texte.

Pour m'aider à lier l'article aux autres ressources, j'ai des fonctions pour lier :

(Hmm, je peux automatiser les liens vers les autres parties de ma configuration qui définissent les autres fonctions…)

Mes pensées sont souvent déconnectées à cause d'un cerveau qui aime sauter d'un sujet à l'autre.​ (Bien évidemment, même avec cet article…) En écrivant un article, j'ajoute de nombreuses idées dans ma boîte de réception. À bien y penser, je pourrais ajouter à Org Mode un genre de lien qui résoudra un vrai lien une fois que l'article lié sera publié, comme les WikiWords, ce qui peut m'aider à les connecter. Je sais qu'il y a des paquets qui offrent cette fonctionnalité.

Publier : J'utilise le générateur de site statique 11ty avec ox-11ty.el. Une fois que je publie une note, le code source org est aussi copié dans le même répertoire.

Visualisation et exploration

J'ai toujours envie de graphes de connaissances comme celui dans le cerveau de Jerry. Je pense que l'affichage du voisinage autour de l'article actuel est plus utile qu'un aperçu global (comme ceux de ) qui est impressionnant mais un peu trop difficile à utiliser.​

J'adore également les grands jardins publics de connaissances comme celui d'Andy Matuschak même si on n'a pas une véritable carte ou un graphe. La richesse des liens m'encourage à les explorer. Je préfère les liens intégrés plutôt que les listes ou les connexions ​sans explications. Je suis curieuse aussi du projet Anagora, qui essaye de créer un grand réseau de graphes de connaissances personnels et de faciliter le saut de l'un à l'autre par sujet.

Mais je ne veux pas investir trop d'efforts pour le faire moi-même. La majorité de mes articles (sauf le bulletin d'information Emacs News et les revues) contiennent peu de liens. Si je reste au niveau des catégories, il y a trop de points près d'Emacs. Je pourrais convertir les sous-catégories dans mon catalogue en données pour la visualisation… Quand même, le catalogue aurait bien besoin d'une mise à jour.

Il y a d'autres genres de graphes que j'utilise fréquemment. Je dessine souvent pendant que j'écris. Je fais des cartes mentales et des notes dessinées. En fait, c'est plutôt que je jette plusieurs mots sur une page et puis que je les déplace, les connecte, et les range graduellement. Je le fais pour découvrir ce que je veux dire et comment je peux les organiser. De temps en temps, j'inclus des dessins dans les articles ou les publie dans mon carnet de croquis public, et c'est une agréable surprise quand un de ces dessins intéresse d'autres personnes.

J'accumule beaucoup de brouillons dans mon fichier posts.org, que je synchronise avec mon téléphone via Syncthing pour éditer sur Orgzly Revived. De temps en temps, j'utilise une carte arborescente (ou treemap) pour visualiser les tailles des brouillons oubliés (et qui sont peut-être presque finalisés, il leur manque seulement une petite révision ou un ajout). (Hmm, je me demande comment je peux ajouter la taille du sous-arbre aux titres d'Org…)

2026-08-29_08-16-41.png
Figure 1: Une capture d'écran de ma carte arborescente pour mon fichier actuel posts.org

En plus, j'apprécie l'analyse des tendances saisonnières ou annuelles dans ma fréquence de publication (qui me rassure sur le fait que les journées chargées de l'été sont normales et je retrouverai bientôt un peu plus de temps libre):

Tendances mensuelles
import json
import seaborn as sns
import re
from collections import defaultdict
import requests
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
with open('/home/sacha/proj/static-blog/_site/blog/all/index.json') as jsonfile:
    posts = json.load(jsonfile)
    jsonfile.close()
monthly_counts = defaultdict(lambda: defaultdict(int))
for post in posts:
    title = post.get("title", "")
    date_str = post.get("date", "")
    if re.search(r'emacs news', title, re.IGNORECASE):
        continue
    if date_str and len(date_str) >= 7:
        year = date_str[:4]
        month = int(date_str[5:7])
        monthly_counts[year][month] += 1
months_labels = ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Juin', 'Juil', 'Août', 'Sept', 'Oct', 'Nov', 'Déc']
years = ['2022', '2023', '2024', '2025', '2026']
df = pd.DataFrame(index=range(1, 13), data={'Mois': months_labels})
for year in years:
    df[year] = [monthly_counts[year][m] for m in range(1, 13)]
df.iloc[8:, -1] = None
df_long = df.melt(id_vars=['Mois'], value_vars=years, var_name='Année', value_name='Compte')
num_old_years = len(years) - 1
gray_shades = np.linspace(0.8, 0.3, num_old_years)  # 0.8 is lighter, 0.3 is darker
custom_colors = {year: str(shade) for year, shade in zip(years[:-1], gray_shades)}
custom_colors['2026'] = "#000000"  # Force the current year to pure black
widths = {year: 1 for year in years}
widths['2026'] = 3
plt.figure(figsize=(10, 5))
ax = sns.lineplot(data=df_long, hue='Année', x='Mois', y='Compte', size='Année', palette=custom_colors, sizes=widths, sort=False)
ax.set_xticks(range(12))
ax.set_xticklabels(months_labels)
plt.title("Fréquence mensuelle des articles sur sachachua.com\n(hors Emacs News)", fontsize=12, fontweight='bold')
plt.xlabel("Mois", fontsize=10)
plt.ylabel("Nombre d'articles publiés", fontsize=10)
plt.grid(True, linestyle='--', alpha=0.5)
plt.legend(loc='upper right')
plt.tight_layout()
plt.savefig('frequence.svg')
return df
  Mois 2022 2023 2024 2025 2026
1 Jan 6 19 20 23 16.0
2 Fév 0 3 0 10 8.0
3 Mar 0 5 1 24 15.0
4 Avr 0 2 1 14 22.0
5 Mai 0 1 1 9 15.0
6 Juin 0 3 1 9 13.0
7 Juil 1 0 0 6 9.0
8 Août 7 2 1 5 11.0
9 Sept 1 8 12 17 nan
10 Oct 2 12 29 11 nan
11 Nov 5 1 19 8 nan
12 Déc 4 15 6 6 nan
2026-08-30T08:43:07.159204 image/svg+xml Matplotlib v3.11.0, https://matplotlib.org/

et la croissance graduelle de mon vocabulaire français selon mon journal:

01_cumulative_vocab.png

Bien qu'ils ne soient pas des graphes de connaissances classiques, ils m'aident à voir des tendances qui ne sont pas évidentes au jour le jour.

Savoir collectif

Je suis plus intéressée par le réseau de savoir collectif plutôt que par mes notes personnelles. Depuis plus de dix ans, je rassemble et catégorise de nombreux liens pour Emacs News, ce qui permet de les parcourir. J'adore rencontrer beaucoup d'idées et de nombreuses personnes en chemin. C'est facile à faire et cela ne demande pas trop de temps, donc j'ai pu continuer ainsi malgré les interruptions de ma vie de maman. En parlant de graphes de connaissances, le bulletin d'information sert à connecter des nœuds et des personnes. De temps en temps, je suis très contente d'entendre qu'un article en inspire un autre, et encore un autre, puis une collaboration… C'est le pouvoir de créer des liens entre les personnes qui apprécient des choses similaires. Si je deviens incapable de faire ce bulletin hebdomadaire, j'espère que quelqu'un le continuera.

Grâce à mon assemblage de liens pour Emacs News, je rencontre souvent des occasions de recommander un article ou une personne en réponse à une question ou un message. Si je peux trouver l'article dans mon fichier d'archive d'Emacs News avec consult-line ou isearch, je peux coller le lien exact. De temps en temps, je veux présenter une personne, ce qui est un peu difficile sur Mastodon parce que les noms de Mastodon sont souvent très différents de leurs vrais noms. Je note les noms de Mastodon dans mon fichier people.org, et j'ai une petite fonction sacha-mastodon-insert-handle-from-contacts pour compléter les noms de Mastodon. J'ai aussi une autre fonction sacha-mastodon-insert-interested-handles qui ajoute des personnes qui peuvent être intéressées sur la base des correspondances d'expressions régulières dans le message, comme quand j'écris mes articles.

En plus des connexions entre des articles et des personnes, je suis également intéressée par les connexions entre des sujets. Avant d'apprendre une chose, quels sujets doivent la précéder ? Quand on apprend une chose, qu'est-ce qui est proche et plus facile à apprendre par la suite ? C'est peut-être utile pour donner des conseils ou des suggestions en tant que coach, ou pour apprendre en mode autodidacte. Les besoins et les chemins diffèrent selon la personne, donc c'est impossible de tracer un parcours qui convient à tout le monde. J'ai commencé à faire une carte des ressources pour les débutants, mais je pense que c'est toujours un peu intimidant, même pour moi. La place naturelle de ces liens est peut-être EmacsWiki pour permettre aux autres personnes de les trouver et de les enrichir, si je trouve une façon de sauvegarder les données et si je résous le problème de l'édition de quelques pages. En plus, si j'étudie les flux de travail et que je les classifie par aspect, je pourrais aider quelqu'un à trouver des ressources qui sont similaires à ses idées.

Je veux aussi identifier des lacunes sur lesquelles me focaliser quand j'ai du temps libre. Il y a beaucoup de ressources pour les débutants, mais grâce à l'immense capacité de personnaliser Emacs, il y a une explosion combinatoire de possibilités au niveau intermédiaire. J'ai hâte d'explorer mes questions et d'autres. C'est difficile de trouver et de naviguer dans les informations, donc si je commence avec des améliorations personnelles ou des questions concrètes de personnes spécifiques, c'est mieux que d'écrire isolément. Je suis sûre qu'en répondant aux questions individuelles, les points communs émergeront.

Je suis aussi fascinée par mes études sur d'autres sujets. J'apprends le français et je m'amuse tellement à bricoler mon environnement Emacs et mon processus d'apprentissage. Un des conseils pour les débutants est de développer des îlots linguistiques : les mots autour d'un sujet passionnant. (Comme cet article-ci que j'écris en français pour me forcer à enrichir mon vocabulaire.) Je me demande comment visualiser ces choses… Par exemple, si je récupère des listes de mots triés par fréquence (peut-être en analysant la base de données lexique), je peux créer une matrice de carrés… Hmm…

Mes obligations de maman diminuent au fur et à mesure que ma fille grandit. J'espère pouvoir consacrer plus de temps à explorer, à saisir et à partager des choses intéressantes.

View Org source for this post

From DC Toedt: Copy Org Mode as Markdown

Posted: - Modified: | emacs, org

: Add embark way to do things.

DC Toedt is a lawyer and professor of practice who uses Emacs and Org Mode. He wanted a small Emacs Lisp function to convert Org Mode syntax to Markdown and copy it to the clipboard to make it easier to copy the materials he's writing for a course on contract drafting. This seems to be a common need, and here are several other approaches:

Anyway, DC shared how he used Claude to generate a simple function to do it, which is here under public domain:

(defun my/org-to-markdown-clipboard ()
  "Export org region (or buffer) to Markdown and copy to clipboard.
With no active region, exports the whole buffer."
  (interactive)
  (require 'ox-md)
  (let* ((text (if (use-region-p)
                   (buffer-substring-no-properties (region-beginning)
(region-end))
                 (buffer-substring-no-properties (point-min) (point-max))))
         (md (org-export-string-as text 'md t '(:with-toc nil
                                                :with-author nil
                                                :with-date nil
                                                :with-title nil))))
    (kill-new md)
    (message "Markdown copied (%d chars)" (length md))))
(with-eval-after-load 'org
  (define-key org-mode-map (kbd "C-c m") #'my/org-to-markdown-clipboard))
View Org source for this post

Create a Google Calendar event from an Org Mode timestamp

| org, emacs

Time zones are hard, so I let calendaring systems take care of the conversion and confirmation. I've been using Google Calendar because it synchronizes with my phone and people know what to do with the event invite. Org Mode has iCalendar export, but I sometimes have a hard time getting .ics files into Google Calendar on my laptop, so I might as well just create the calendar entry in Google Calendar directly. Well. Emacs is a lot more fun than Google Calendar, so I'd rather create the calendar entry from Emacs and put it into Google Calendar.

This function lets me start from a timestamp like [2026-04-24 Fri 10:30] (inserted with C-u C-c C-!, or org-timestamp-inactive) and create an event based on a template.

(defvar sacha-time-zone "America/Toronto" "Full name of time zone.")

;;;###autoload
(defun sacha-emacs-chat-schedule (&optional time)
  "Create a Google Calendar invite based on TIME or the Org timestamp at point."
  (interactive (list (sacha-org-time-at-point)))
  (browse-url
   (format
    "https://calendar.google.com/calendar/render?action=TEMPLATE&text=%s&details=%s&dates=%s&ctz=%s"
    (url-hexify-string sacha-emacs-chat-title)
    (url-hexify-string sacha-emacs-chat-description)
    (format-time-string
     "%Y%m%dT%H%M%S" time)
    sacha-time-zone)))

(defvar sacha-emacs-chat-title "Emacs Chat" "Title of calendar entry.")
(defvar sacha-emacs-chat-description
  "All right, let's try this! =) See the calendar invite for the Google Meet link.

Objective: Share cool stuff about Emacs workflows that's not obvious from reading configs, and have fun chatting about Emacs

Some ideas for things to talk about:
- Which keyboard shortcuts or combinations of functions work really well for you?
- What's something you love about your setup?
- What are you looking forward to tweaking next?

Let me know if you want to do it on stream (more people can ask questions) or off stream (we can clean up the video in case there are hiccups). Also, please feel free to send me links to things you'd like me to read ahead of time, like your config!"
  "Description.")

It uses this function to convert the timestamp at point:

sacha-org-time-at-point: Return Emacs time object for timestamp at point.
(defun sacha-org-time-at-point ()
  "Return Emacs time object for timestamp at point."
  (org-timestamp-to-time (org-timestamp-from-string (org-element-property :raw-value (org-element-context)))))

This is part of my Emacs configuration.
View Org source for this post

Make chapter markers and video time hyperlinks easier to note while I livestream

| org, emacs

I want to make it easier to add chapter markers to my YouTube video descriptions and hyperlinks to specific times in videos in my blog posts.

This is part of my Emacs configuration.
View Org source for this post

Org Mode: JS for translating times to people's local timezones

| org, emacs, js

I want to get back into the swing of doing Emacs Chats again, which means scheduling, which means timezones. Let's see first if anyone happens to match up with the Thursday timeslots (10:30 or 12:45) that I'd like to use for Emacs-y video things, but I might be able to shuffle things around if needed.

I want something that can translate times into people's local timezones. I use Org Mode timestamps a lot because they're so easy to insert with C-u C-c ! (org-timestamp-inactive), which inserts a timestamp like this:

By default, the Org HTML export for it does not include the timezone offset. That's easily fixed by adding %z to the time specifier, like this:

(setq org-html-datetime-formats '("%F" . "%FT%T%z"))

Now a little bit of Javascript code makes it clickable and lets us toggle a translated time. I put the time afterwards so that people can verify it visually. I never quite trust myself when it comes to timezone translations.

function translateTime(event) {
  if (event.target.getAttribute('datetime')?.match(/[0-9][0-9][0-9][0-9]$/)) {
    if (event.target.querySelector('.translated')) {
      event.target.querySelectorAll('.translated').forEach((o) => o.remove());
    } else {
      const span = document.createElement('span');
      span.classList.add('translated');
      span.textContent = ' → ' + (new Date(event.target.getAttribute('datetime'))).toLocaleString(undefined, {
        month: 'short',  
        day: 'numeric',  
        hour: 'numeric', 
        minute: '2-digit',
        timeZoneName: 'short'
      });
      event.target.appendChild(span);
    }
  }
}
function clickForLocalTime() {
  document.querySelectorAll('time').forEach((o) => {
    if (o.getAttribute('datetime')?.match(/[0-9][0-9][0-9][0-9]$/)) {
      o.addEventListener('click', translateTime);
      o.classList.add('clickable');
    }
  });
}

And some CSS to make it more obvious that it's now clickable:

.clickable {
    cursor: pointer;
    text-decoration: underline dotted;
}

Let's see if this is useful.

Someday, it would probably be handy to have a button that translates all the timestamps in a table, but this is a good starting point.

View Org source for this post

Org Mode: Tangle Emacs config snippets to different files and add boilerplate

| emacs, org

I want to organize the functions in my Emacs configuration so that they are easier for me to test and so that other people can load them from my repository. Instead of copying multiple code blogs from my blog posts or my exported Emacs configuration, it would be great if people could just include a file from the repository. I don't think people copy that much from my config, but it might still be worth making it easier for people to borrow interesting functions. It would be great to have libraries of functions that people can evaluate without worrying about side effects, and then they can copy or write a shorter piece of code to use those functions.

In Prot's configuration (The custom libraries of my configuration), he includes each library as in full, in a single code block, with the boilerplate description, keywords, and (provide '...) that make them more like other libraries in Emacs.

I'm not quite sure my little functions are at that point yet. For now, I like the way that the functions are embedded in the blog posts and notes that explain them, and the org-babel :comments argument can insert links back to the sections of my configuration that I can open with org-open-at-point-global or org-babel-tangle-jump-to-org.

Thinking through the options...

Org tangles blocks in order, so if I want boilerplate or if I want to add require statements, I need to have a section near the beginning of my config that sets those up for each file. Noweb references might help me with common text like the license. Likewise, if I want a (provide ...) line at the end of each file, I need a section near the end of the file.

If I want to specify things out of sequence, I could use Noweb. By setting :noweb-ref some-id :tangle no on the blocks I want to collect later, I can then tangle them in the middle of the boilerplate. Here's a brief demo:

#+begin_src emacs-lisp :noweb yes :tangle lisp/sacha-eshell.el :comments no
;; -*- lexical-binding: t; -*-
<<sacha-eshell>>
(provide 'sacha-eshell)
#+end_src

However, I'll lose the comment links that let me jump back to the part of the Org file with the original source block. This means that if I use find-function to jump to the definition of a function and then I want to find the outline section related to it, I have to use a function that checks if this might be my custom code and then looks in my config for "defun …". It's a little less generic.

I wonder if I can combine multiple targets with some code that knows what it's being tangled to, so it can write slightly different text. org-babel-tangle-single-block currently calculates the result once and then adds it to the list for each filename, so that doesn't seem likely.

Alternatively, maybe I can use noweb or my own tangling function and add the link comments from org-babel-tangle-comments.

Aha, I can fiddle with org-babel-post-tangle-hook to insert the boilerplate after the blocks have been written. Then I can add the lexical-binding: t cookie and the structure that makes it look more like the other libraries people define and use. It's always nice when I can get away with a small change that uses an existing hook. For good measure, let's even include a list of links to the sections of my config that affect that file.

(defvar sacha-dotemacs-url "https://sachachua.com/dotemacs/")

;;;###autoload
(defun sacha-dotemacs-link-for-section-at-point (&optional combined)
  "Return the link for the current section."
  (let* ((custom-id (org-entry-get-with-inheritance "CUSTOM_ID"))
         (title (org-entry-get (point) "ITEM"))
         (url (if custom-id
                  (concat "dotemacs:" custom-id)
                (concat sacha-dotemacs-url ":-:text=" (url-hexify-string title)))))
    (if combined
        (org-link-make-string
         url
         title)
      (cons url title))))

(eval-and-compile
  (require 'org-core nil t)
  (require 'org-macs nil t)
  (require 'org-src nil t))
(declare-function 'org-babel-tangle--compute-targets "ob-tangle")
(defun sacha-org-collect-links-for-tangled-files ()
  "Return a list of ((filename (link link link link)) ...)."
  (let* ((file (buffer-file-name))
         results)
    (org-babel-map-src-blocks (buffer-file-name)
      (let* ((info (org-babel-get-src-block-info))
             (link (sacha-dotemacs-link-for-section-at-point)))
        (mapc
         (lambda (target)
           (let ((list (assoc target results #'string=)))
             (if list
                 (cl-pushnew link (cdr list) :test 'equal)
               (push (list target link) results))))
         (org-babel-tangle--compute-targets file info))))
    ;; Put it back in source order
    (nreverse
     (mapcar (lambda (o)
               (cons (car o)
                     (nreverse (cdr o))))
             results))))
(defvar sacha-emacs-config-module-links nil "Cache for links from tangled files.")

;;;###autoload
(defun sacha-emacs-config-update-module-info ()
  "Update the list of links."
  (interactive)
  (setq sacha-emacs-config-module-links
        (seq-filter
         (lambda (o)
           (string-match "sacha-" (car o)))
         (sacha-org-collect-links-for-tangled-files)))
  (setq sacha-emacs-config-modules-info
        (mapcar (lambda (group)
                  `(,(file-name-base (car group))
                    (commentary
                     .
                     ,(replace-regexp-in-string
                       "^"
                       ";; "
                       (concat
                        "Related Emacs config sections:\n\n"
                        (org-export-string-as
                         (mapconcat
                          (lambda (link)
                            (concat "- " (cdr link) "\\\\\n  " (org-link-make-string (car link)) "\n"))
                          (cdr group)
                          "\n")
                         'ascii
                         t))))))
                sacha-emacs-config-module-links)))

;;;###autoload
(defun sacha-emacs-config-prepare-to-tangle ()
  "Update module info if tangling my config."
  (when (string-match "Sacha.org" (buffer-file-name))
    (sacha-emacs-config-update-module-info)))

Let's set up the functions for tangling the boilerplate.

(defvar sacha-emacs-config-modules-dir "~/sync/emacs/lisp/")
(defvar sacha-emacs-config-modules-info nil "Alist of module info.")
(defvar sacha-emacs-config-url "https://sachachua.com/dotemacs")

;;;###autoload
(defun sacha-org-babel-post-tangle-insert-boilerplate-for-sacha-lisp ()
  (when (file-in-directory-p (buffer-file-name) sacha-emacs-config-modules-dir)
    (goto-char (point-min))
    (let ((base (file-name-base (buffer-file-name))))
      (insert (format ";;; %s.el --- %s -*- lexical-binding: t -*-

;; Author: %s <%s>
;; URL: %s

;;; License:
;;
;; This file is not part of GNU Emacs.
;;
;; This is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.
;;
;; This is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING.  If not, write to the
;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
;; Boston, MA 02110-1301, USA.

;;; Commentary:
;;
%s
;;; Code:

\n\n"
                      base
                      (or
                       (assoc-default 'description
                                      (assoc-default base sacha-emacs-config-modules-info #'string=))
                       "")
                      user-full-name
                      user-mail-address
                      sacha-emacs-config-url
                      (or
                       (assoc-default 'commentary
                                      (assoc-default base sacha-emacs-config-modules-info #'string=))
                       "")))
      (goto-char (point-max))
      (insert (format "\n(provide '%s)\n;;; %s.el ends here\n"
                      base
                      base))
      (save-buffer))))
(setq sacha-emacs-config-url "https://sachachua.com/dotemacs")
(with-eval-after-load 'org
  (add-hook 'org-babel-pre-tangle-hook #'sacha-emacs-config-prepare-to-tangle)
  (add-hook 'org-babel-post-tangle-hook #'sacha-org-babel-post-tangle-insert-boilerplate-for-sacha-lisp))

You can see the results at .emacs.d/lisp. For example, the function definitions in this post are at lisp/sacha-emacs.el.

This is part of my Emacs configuration.
View Org source for this post

YE11: Fix find-function for Emacs Lisp from org-babel or scratch

| org, emacs, elisp, stream, yay-emacs

Watch on Internet Archive, watch/comment on YouTube, download captions, or email me

Where can you define an Emacs Lisp function so that you can use find-function to jump to it again later?

  • A: In an indirect buffer from Org Mode source block with your favorite eval function like eval-defun
    • C-c ' (org-edit-special) inside the block; execute the defun with C-M-x (eval-defun), C-x C-e (eval-last-sexp), or eval-buffer.

          (defun my-test-1 () (message "Hello"))
      
  • B: In an Org Mode file by executing the block with C-c C-c

      (defun my-test-2 () (message "Hello"))
    
  • C: In a .el file

    file:///tmp/test-search-function.el : execute the defun with C-M-x (eval-defun), C-x C-e (eval-last-sexp), or eval-buffer

  • D: In a scratch buffer, other temporary buffer, or really any buffer thanks to eval-last-sexp

    (defun my-test-4 () (message "Hello"))

Only option C works - it's gotta be in an .el file for find-function to find it. But I love jumping to function definitions using find-function or lispy-goto-symbol (which is bound to M-. if you use lispy and set up lispy-mode) so that I can look at or change how something works. It can be a little frustrating when I try to jump to a definition and it says, "Don't know where blahblahblah is defined." I just defined it five minutes ago! It's there in one of my other buffers, don't make me look for it myself. Probably this will get fixed in Emacs core someday, but no worries, we can work around it today with a little bit of advice.

I did some digging around in the source code. Turns out that symbol-file can't find the function definition in the load-history variable if you're not in a .el file, so find-function-search-for-symbol gets called with nil for the library, which causes the error. (emacs:subr.el)

I wrote some advice that searches in any open emacs-lisp-mode buffers or in a list of other files, like my Emacs configuration. This is how I activate it:

(setq sacha-elisp-find-function-search-extra '("~/sync/emacs/Sacha.org"))
(advice-add 'find-function-search-for-symbol :around #'sacha-elisp-find-function-search-for-symbol)

Now I should be able to jump to all those functions wherever they're defined.

(my-test-1)
(my-test-2)
(my-test-3)
(my-test-4)

Note that by default, M-. in emacs-lisp-mode uses xref-find-definitions, which seems to really want files. I haven't figured out a good workaround for that yet, but lispy-mode makes M-. work and gives me a bunch of other great shortcuts, so I'd recommend checking that out.

Here's the source code for the find function thing:

(defvar sacha-elisp-find-function-search-extra
  nil
  "List of filenames to search for functions.")

;;;###autoload
(defun sacha-elisp-find-function-search-for-symbol (fn symbol type library &rest _)
  "Find SYMBOL with TYPE in Emacs Lisp buffers or `sacha-find-function-search-extra'.
Prioritize buffers that do not have associated files, such as Org Src
buffers or *scratch*. Note that the fallback search uses \"^([^ )]+\" so that
it isn't confused by preceding forms.

If LIBRARY is specified, fall back to FN.

Activate this with:

(advice-add 'find-function-search-for-symbol
 :around #'sacha-org-babel-find-function-search-for-symbol-in-dotemacs)"
  (if (null library)
      ;; Could not find library; search my-dotemacs-file just in case
      (progn
        (while (and (symbolp symbol) (get symbol 'definition-name))
          (setq symbol (get symbol 'definition-name)))
        (catch 'found
          (mapc
           (lambda (buffer-or-file)
             (with-current-buffer (if (bufferp buffer-or-file)
                                      buffer-or-file
                                    (find-file-noselect buffer-or-file))
               (let* ((regexp-symbol
                       (or (and (symbolp symbol)
                                (alist-get type (get symbol 'find-function-type-alist)))
                           (alist-get type find-function-regexp-alist)))
                      (form-matcher-factory
                       (and (functionp (cdr-safe regexp-symbol))
                            (cdr regexp-symbol)))
                      (regexp-symbol (if form-matcher-factory
                                         (car regexp-symbol)
                                       regexp-symbol))

                      (case-fold-search)
                      (regexp (if (functionp regexp-symbol) regexp-symbol
                                (format (symbol-value regexp-symbol)
                                        ;; Entry for ` (backquote) macro in loaddefs.el,
                                        ;; (defalias (quote \`)..., has a \ but
                                        ;; (symbol-name symbol) doesn't.  Add an
                                        ;; optional \ to catch this.
                                        (concat "\\\\?"
                                                (regexp-quote (symbol-name symbol)))))))
                 (save-restriction
                   (widen)
                   (with-syntax-table emacs-lisp-mode-syntax-table
                     (goto-char (point-min))
                     (if (if (functionp regexp)
                             (funcall regexp symbol)
                           (or (re-search-forward regexp nil t)
                               ;; `regexp' matches definitions using known forms like
                               ;; `defun', or `defvar'.  But some functions/variables
                               ;; are defined using special macros (or functions), so
                               ;; if `regexp' can't find the definition, we look for
                               ;; something of the form "(SOMETHING <symbol> ...)".
                               ;; This fails to distinguish function definitions from
                               ;; variable declarations (or even uses thereof), but is
                               ;; a good pragmatic fallback.
                               (re-search-forward
                                (concat "^([^ )]+" find-function-space-re "['(]?"
                                        (regexp-quote (symbol-name symbol))
                                        "\\_>")
                                nil t)))
                         (progn
                           (beginning-of-line)
                           (throw 'found
                                   (cons (current-buffer) (point))))
                       (when-let* ((find-expanded
                                    (when (trusted-content-p)
                                      (find-function--search-by-expanding-macros
                                       (current-buffer) symbol type
                                       form-matcher-factory))))
                         (throw 'found
                                 (cons (current-buffer)
                                       find-expanded)))))))))
           (delq nil
                 (append
                  (sort
                   (match-buffers '(derived-mode . emacs-lisp-mode))
                   :key (lambda (o) (or (buffer-file-name o) "")))
                  sacha-elisp-find-function-search-extra)))))
    (funcall fn symbol type library)))

I even figured out how to write tests for it:

(ert-deftest sacha-elisp--find-function-search-for-symbol--in-buffer ()
  (let ((sym (make-temp-name "--test-fn"))
        buffer)
    (unwind-protect
        (with-temp-buffer
          (emacs-lisp-mode)
          (insert (format ";; Comment\n(defun %s () (message \"Hello\"))" sym))
          (eval-last-sexp nil)
          (setq buffer (current-buffer))
          (with-temp-buffer
            (let ((pos (sacha-elisp-find-function-search-for-symbol nil (intern sym) nil nil)))
              (should (equal (car pos) buffer))
              (should (equal (cdr pos) 12)))))
      (fmakunbound (intern sym)))))

(ert-deftest sacha-elisp--find-function-search-for-symbol--in-file ()
  (let* ((sym (make-temp-name "--test-fn"))
         (temp-file (make-temp-file
                     "test-" nil ".org"
                     (format
                      "#+begin_src emacs-lisp\n;; Comment\n(defun %s () (message \"Hello\"))\n#+end_src"
                      sym)))
         (sacha-elisp-find-function-search-extra (list temp-file))
         buffer)
    (unwind-protect
        (with-temp-buffer
          (let ((pos (sacha-elisp-find-function-search-for-symbol nil (intern sym) nil nil)))
            (should (equal (buffer-file-name (car pos)) temp-file))
            (should (equal (cdr pos) 35))))
      (delete-file temp-file))))
This is part of my Emacs configuration.
View Org source for this post