Sept mois à la recherche d’un éditeur

VS Code a été mon premier et unique éditeur. Mais ses performances me gênaient : parfois, il consomme énormément de processeur et de mémoire. J’ai donc commencé à chercher quelque chose de plus rapide.

Mes exigences étaient modestes : un aperçu pour les PDF et les images, un terminal, et Git dans l’éditeur.

Les éditeurs en terminal sont rapides, mais aveugles

Le premier chemin menait au terminal. Là, la vitesse n’est pas un problème. L’aperçu, si : pas d’image, pas de PDF, pas de Markdown rendu.

J’ai donc essayé de l’ajouter après coup. Dans Fresh, les plugins sont trop limités pour cela. Dans LazyVim, cela fonctionne, mais pas partout. Tous les terminaux ne suivent pas, et ce n’était pas stable non plus : LazyVim plantait lors des aperçus d’images dans l’explorateur. Par-dessus tout planait le pressentiment que je finirais par buter contre un plafond inscrit dans le terminal lui-même.

Changer l’enveloppe plutôt que rafistoler le plugin

C’est de là qu’est née ma première approche personnelle, et elle inverse le problème. Si l’aperçu ne rentre pas dans le terminal, alors c’est l’éditeur en terminal qui doit entrer dans une fenêtre.

Deux tentatives sont allées dans ce sens. neoview intègre Neovim via nvim-rs dans une interface construite avec gpui, le framework avec lequel Zed est aussi construit. L’interface communique avec Neovim via msgpack-rpc. freshview fait la même chose avec l’éditeur Fresh, mais de manière plus étroite : Fresh tourne comme bibliothèque dans le même processus, egui fournit la fenêtre, egui_ratatui traduit entre les deux, et mupdf dessine les PDF et les images dans des fenêtres flottantes à côté.

Les tentatives suivantes, flexed et editor-framework, ne sont plus des wrappers, mais des squelettes sur gpui dans lesquels rien n’est noyau et tout est plugin. Dans editor-framework, les plugins sont écrits en Lua et se rechargent sans recompilation.

Pourquoi Rust a été écarté

Zed est un bon éditeur, mais sans aperçu PDF. Et Zed est écrit en Rust.

Rust était trop lent à compiler pour moi. neoview et freshview avaient leurs difficultés, un affichage lent ici, un plantage à la fermeture de l’aperçu PDF là. On aurait peut-être pu les maîtriser, peut-être pas. C’est autre chose qui a tranché : avec ces temps de compilation, je n’avançais pas assez vite. Et gpui, c’est du Rust, donc il est parti avec.

Je cherchais un autre langage rapide, avec lequel on puisse concurrencer Zed. Go paraissait poussiéreux, V trop peu connu, et Zig était justement en pleine percée comme concurrent de Rust. Donc Zig, et là, la compilation est pour moi agréablement plus rapide.

Onze tentatives, huit interfaces

Chaque ligne est un dépôt à part, dans l’ordre dans lequel ils ont été créés. Certains ont vécu un jour, d’autres une semaine. Les onze tiennent en cinq semaines, entre début mars et début avril 2026.

DébutTentativeLangageInterfaceRaison de l’abandon
2 marsneoviewRustgpuiTemps de compilation
2 marsfreshviewRustegui, egui_ratatuiTemps de compilation
8 marsflexedRustgpuiTemps de compilation
9 marseditor-frameworkRustgpui, mluaTemps de compilation
14 marsslint-rust-editorRustSlintTemps de compilation
15 marsmojo-nuklear-editorMojo, pont CNuklearMojo inadapté pour écrire des éditeurs
17 marsslint-editorMojoSlint via PythonSlint accessible uniquement via Python
22 marsv-gui-editorVFramework GUI de VLe défilement a cassé après l’ajout de l’explorateur et des onglets
25 marszed-cloneZigdvuiPlantage au défilement, gros fichiers lents
26 marssokol-nanovg-zig-editorZigsokol, NanoVGPolice disparue après le passage à Vulkan, la boucle de jeu de sokol coûtait du CPU
2 avrilqt-zigedZigQt 6 via libqt6zigResté une démonstration technique

Le 3 avril a commencé zid. Depuis, il n’y a plus eu de nouvelle tentative.

Ce qui en est sorti

L’éditeur s’appelle zid et est écrit en Zig. Il dessine via wgpu, avec le renderer fixé sur le backend Vulkan. La mise en page est assurée par Clay. Le rendu du texte est maison : atlas de glyphes et renderer GPU basé sur FreeType.

L’éditeur lui-même est maison : saisie, commandes d’édition, recherche, retour à la ligne, numéros de ligne, raccourcis clavier. En dessous se trouve du code tiers, et c’est voulu. Le stockage du texte est assuré par flow-core, la bibliothèque centrale de l’éditeur Flow Control, avec les types de tampon, de curseur et de sélection. La coloration syntaxique vient de la même maison via tree-sitter. Le terminal est l’émulation de Ghostty, le Markdown est rendu par zigdown, le PDF est dessiné par mupdf.

C’est un autre découpage que pour neoview et freshview. Là, tout l’éditeur était tiers et seule l’enveloppe était maison. Avec zid, c’est l’inverse : l’éditeur est maison, et ce qui est tiers, ce sont les briques en dessous, qui n’ont rien à voir avec l’édition.

Les règles ont leur place dans le code, pas dans le prompt

zid a un agent intégré qui travaille avec un modèle de langage local, llama-server avec Qwen3-4B. Un agent dans l’éditeur a besoin d’outils, et les outils ont besoin de limites. La voie évidente consiste à écrire les limites dans le prompt système. Mais avec un modèle de cette taille, une ligne de prompt n’est qu’une prière, et elle coûte de la place à chaque requête dans un contexte de 8192 tokens. C’est pourquoi les limites sont dans le code. Là, en revanche, elles doivent être sans faille.

Un exemple. Qui veut écraser un fichier existant reçoit une boîte de dialogue de confirmation. Le modèle l’a contournée dès la première tentative : au lieu de write_file, il a envoyé un replace_text dont le texte recherché était le fichier entier. Formellement pas un écrasement, en pratique exactement cela.

Depuis, replace_text compte aussi comme écrasement dès qu’il remplace la moitié du fichier ou plus :

/// replace_text mit `old` = (fast) ganzer Datei ist ein verkapptes Überschreiben und
/// braucht dieselbe Bestätigung wie write_file auf eine bestehende Datei.
pub fn replaceCountsAsRewrite(file_len: usize, old_len: usize) bool {
    if (file_len == 0) return false;
    return old_len * 2 >= file_len;
}

Il en va de même pour les chemins. Au lieu de dire au modèle de rester dans le projet, une fonction résout le chemin et ne renvoie rien s’il se trouve à l’extérieur. Un chemin qui sort du projet via .. ou en absolu ne peut alors produire aucune opération sur un fichier.

De telles règles sont des fonctions pures, et les fonctions pures se testent. Les deux ont des tests unitaires. S’y ajoute un script de bout en bout qui fait tourner l’éditeur en headless contre le vrai modèle et vérifie le chemin du refus : demander un écrasement, attendre la boîte de dialogue, refuser, fichier inchangé, et un chemin hors du projet est rejeté. Une ligne de prompt, on ne peut que l’espérer.

À l’inverse, il en va de même pour les capacités. L’outil command reçoit ses choix générés à partir de l’énumération des commandes de l’éditeur. Chaque menu et chaque raccourci clavier est ainsi automatiquement accessible à l’agent, sans qu’une deuxième liste doive être tenue à jour quelque part.

La question de savoir dans quelle fenêtre apparaît un fichier ouvert par l’agent n’est pas non plus une instruction au modèle, mais une fonction avec un test unitaire. Le prompt système s’est réduit à trois phrases : le rôle, la convention selon laquelle les chemins s’entendent relativement au projet, et la demande de répondre brièvement dans la langue de l’utilisateur après les résultats des outils. Tout le reste est dans le code, où c’est vérifié, au lieu d’être dans le prompt, où c’est demandé.

Ce qui reste ouvert

Il y a des limites qui restent en place volontairement. L’éditeur charge exactement une graisse de police, c’est pourquoi la vue Markdown affiche le gras et l’italique par des couleurs au lieu de vraies variantes de police. Ces points figurent comme décisions dans la documentation du projet, pas comme tâches ouvertes.

Où en est le projet aujourd’hui

Sur les trois exigences de l’introduction, deux sont remplies : les PDF et les images s’ouvrent dans un onglet, le terminal est intégré. Git n’est là qu’à moitié. L’explorateur affiche le statut de chaque fichier, diff et blame manquent encore. Se sont ajoutées des choses qui n’étaient pas sur la liste : aperçu Markdown, diapositives à partir de Markdown avec export en PDF, saut à la définition via un serveur de langage, ouverture rapide et palette de commandes, et l’agent. Il est compilé pour Linux et Windows.

Le code source est ouvert : github.com/gstrainovic/zid. Les onze tentatives précédentes sont également publiques, chacune sous son nom dans le tableau.

Comment l’agent pilote et vérifie l’éditeur sans fenêtre, c’est l’objet du prochain article : Un agent IA teste mon éditeur sans fenêtre.