Stores et synchronisation
Le problème à éviter
Un IDE affiche plusieurs vues d'un même état : l'arborescence, les onglets ouverts, le statut Git, les diffs. Quand un fichier change (sauvegarde, commande du terminal, checkout Git, import d'un projet), toutes doivent suivre.
La première version de CapyCode laissait chaque appelant rafraîchir lui-même ce qu'il pensait concerné (refreshTree(), gitStore.refresh(), reloadOpenTabs(), notifyFileRenamed()…). Résultat : des dizaines d'appels dispersés, des oublis (le statut Git ne suivait pas une sauvegarde) et des doublons (l'arborescence relue à chaque Ctrl+S, l'historique Git recalculé pour indexer un fichier).
Le principe : les services notifient, les stores s'abonnent
Deux services sont la source de vérité :
vfsnotifie toute modification du workspace :write,mkdir,delete,rename, oureset(contenu changé de façon quelconque : checkout, import de projet). isomorphic-git écrivant directement dans LightningFS, le service Git appellevfs.notifyChange()après un checkout ou une fusion.gitServicenotifie ses opérations :index(indexer, désindexer) ourefs(commit, branches, tags, checkout).
Chaque store s'abonne à ce qui le concerne, et à rien d'autre :
| Store | Écoute | Réaction |
|---|---|---|
fileSystem | vfs | relit l'arborescence, une fois par lot |
git | vfs, gitService | fichier de travail modifié ou index → statut seul ; .git touché, reset ou refs → tout (branches, tags, statut, historique) |
editor | vfs, gitService | renomme ou ferme les onglets, relit les fichiers modifiés hors de l'éditeur, met à jour les diffs |
Les appelants n'ont plus rien à rafraîchir : ils écrivent, c'est tout.
Une écriture se termine quand l'IDE est à jour
Les abonnés sont appelés de façon asynchrone mais attendus : await vfs.writeTextFile(…) ne se résout qu'une fois l'arborescence, le statut Git et les onglets à jour. Le code reste séquentiel et prévisible, et les tests n'ont pas besoin d'attendre « un peu ».
Les lots
Une opération en masse (copie récursive, import ZIP, rejeu d'une activité partagée) ne doit pas déclencher un rafraîchissement par fichier :
vfs.batch(task)diffère les notifications du VFS jusqu'à la fin detask, puis les envoie en un seul lot ;gitService.batch(task)fait de même pour Git (une seule notificationrefsà la fin) ;- chaque commande du terminal est exécutée dans un
vfs.batch.
Enfin, les rafraîchissements concurrents sont regroupés (src/utils/coalesce.ts) : si dix notifications arrivent pendant qu'un statut Git est calculé, il n'y aura qu'un seul calcul de plus.
Les stores
Seules ces dépendances existent entre stores ; aucune ne sert à la synchronisation. fileSystem, git et editor ne dépendent d'aucun autre store.
| Store | Rôle |
|---|---|
fileSystem | Arborescence du workspace, dossiers dépliés, fichiers cachés, CRUD de fichiers |
editor | Onglets (deux volets), contenus et état modifié, diffs, formatage, sauts de ligne |
git | Dépôt, branches, tags, statut, historique, opérations Git de l'interface |
runtime | Exécution : sessions des prompts et des terminaux, bouton Exécuter, évaluation, interruption (voir Basthon) ; ne décide pas de l'affichage |
console | Consoles du panneau inférieur : terminaux, prompts, sorties graphiques ; décide où s'affiche une exécution (prompt à ouvrir, console graphique, panneau déplié, mise en forme des résultats) |
terminal | Une session par terminal : lignes, répertoire courant, historique, invite, sortie au fil de l'eau, input(), programmes Python (voir Terminal) |
diagnostics | Diagnostics Ruff par fichier, compteurs de la barre d'état |
extensions | Extensions intégrées activées (linter, git, formatter, outline, sql, ocaml), disponibilité, persistance |
embedding | Options réglées par la page hôte (Git activé ou non), voir Intégration dans une page hôte |
layout, theme, i18n, pwa, commandPalette | Préférences et état d'interface |
Le store editor
C'est le plus riche : il est composé de modules (src/stores/editor/), assemblés par index.ts.
| Module | Rôle |
|---|---|
panes.ts | Onglets des deux volets, scission, volet actif, glisser-déposer d'onglets |
documents.ts | Contenus ouverts, saisie, sauvegarde, suivi du workspace (fichier renommé, supprimé, modifié ailleurs) |
specialTabs.ts | Onglets de comparaison (index, copie de travail, commit) et graphe Git |
formatting.ts | Formatage Ruff, résumé et annulation |
requests.ts | Contrôleurs des éditeurs CodeMirror (annuler, copier, rechercher…, enregistrés par chaque volet), saut à une ligne et position du curseur |
Chaque module est une fonction qui reçoit ce dont elle dépend (par exemple createDocuments(contents, panes, onSynced)) ; seul index.ts appelle defineStore.
Quelques choix d'efficacité
- Un seul parcours du disque par lot. L'arborescence complète est lue en une fois ; la liste des fichiers (palette, complétion) en est dérivée.
- Déplier un dossier ne relit rien : l'état déplié est appliqué à l'arborescence en mémoire. Seul
.git, qui contient des milliers d'objets, n'est lu qu'à son premier dépliage. - Renommer un dossier est instantané (
vfs.rename, natif dans LightningFS) au lieu d'une copie fichier par fichier. - Le statut d'un fichier est indexé par chemin : l'explorateur l'interroge pour chaque nœud sans parcourir la liste.
Ajouter une fonctionnalité qui modifie le workspace
- Écrire par
vfs(ougitServicepour une opération Git). Pour plusieurs écritures, les envelopper dansvfs.batch. - Ne rien rafraîchir à la main.
- Si un nouvel état doit suivre le workspace, l'abonner dans son store avec
onScopeDispose(vfs.onChange(…)).