turbo-editors/turbo-golopublic Fork 0
v1.0.0
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-golo.git
git clone ssh://git@rickub.com/turbo-editors/turbo-golo.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

📦 Turbo Golo d710c1b · on v1.0.0 · k33g · 12h ago
architecture.md · 111 lines · 11.4 KBmarkdown
Blame HistoryOpen raw

Architecture — explication

De quoi s'agit-il ?

Turbo Golo, c'est une commande, un profil et un scanner. Tout le reste — le widget d'édition, les fenêtres, les menus, les dialogues, les thèmes, l'émulateur de terminal, l'arbre de fichiers, le client LSP — c'est turbo-core, la bibliothèque sur laquelle tous les éditeurs Turbo sont construits.

Cette page parle de cette séparation : ce qui est ici, ce qui est là-bas, et pourquoi la frontière passe où elle passe.

Ce que contient ce dépôt

main.go                  les options, le terminal et le câblage
internal/gololang        tout ce qui fait de cet éditeur Turbo Golo
  gololang.go            le profil : nom, menu, serveur, où golo est installé
  scan.go                le répartiteur du scanner, les commentaires, ce qui franchit une ligne
  literals.go            les trois formes entre guillemets — "…", """…""" et '…'
  words.go               les nombres, les mots-clés, les 157 builtins, les conventions de nommage
  templates.go           trois déclarations //go:embed
  *.toml.tmpl            les trois fichiers de départ d'un projet, embarqués

Un millier de lignes environ en comptant les commentaires, dont quelque six cents pour le scanner — moins de cinq cents lignes de code au compte de qlty, et un tiers de celles-ci est la table des builtins. Il n'y a pas d'internal/app, pas d'internal/ui, pas d'internal/buffer : ceux-là existent une fois, dans la bibliothèque, et les six éditeurs les utilisent tels quels.

Ce que fait main

Six choses, dans cet ordre :

  1. Il lit les options.
  2. Il appelle gololang.Register(), qui apprend à la bibliothèque à colorer les fichiers .golo et les scripts dont la première ligne nomme golo.
  3. Il construit gololang.Profile() — la valeur qui dit que cet éditeur est Turbo Golo.
  4. Il lit .turbo-golo/settings.toml dans le répertoire courant, s'il existe.
  5. Il ouvre le terminal et confie l'écran, le nom du thème et le profil à app.New.
  6. Il démarre golo lsp dans le répertoire du fichier édité, et lance la boucle d'événements.

C'est toute la commande. Chaque décision qu'elle prend — quel thème l'emporte, quels fichiers ouvrir, faut-il démarrer un serveur de langage — porte sur cette exécution, pas sur Golo.

Le profil est la couture

profile.Profile{
	Name:        "Turbo Golo",
	Slug:        "turbo-golo",
	Language:    "Golo",
	ToolsMenu:   "~G~olo",
	RootMarkers: nil,
	Server:      profile.Server{Command: "golo", Args: []string{"lsp"}, …},
	Templates:   profile.Templates{Settings: …, Snippets: …, Tools: …},
}

Tout ce qui serait sinon un "turbo-golo", un "golo" ou un ".golo" codé en dur quelque part dans onze mille lignes est un champ ici. La bibliothèque les lit ; rien dans la bibliothèque ne sait ce qu'ils signifient.

Slug porte plus qu'il n'y paraît. Le binaire s'appelle turbo-golo, le répertoire de projet .turbo-golo, la configuration personnelle vit dans ~/.config/turbo-golo, et les variables d'environnement qui la remplacent sont TURBO_GOLO_THEME_DIR et TURBO_GOLO_SNIPPET_DIR — toutes dérivées de ce seul mot.

RootMarkers est le seul champ vide ici et rempli chez tous les frères. Go a go.mod, Rust Cargo.toml, Python pyproject.toml, MoonBit moon.mod ; Golo n'a aucun manifeste. Un script est un fichier et un programme est un répertoire de fichiers, il n'y a donc rien vers quoi remonter, et le ProjectRoot de la bibliothèque — sans marqueur — répond par le répertoire du fichier. C'est aussi tout ce dont golo lsp a besoin : il répond à propos du fichier qu'on lui donne et résout les imports depuis les modules embarqués dans le binaire, jamais depuis le disque.

Pourquoi le scanner est ici et pas dans la bibliothèque

turbo-core colore huit langages lui-même : TOML, YAML, Markdown, JavaScript, HTML, XML, les Dockerfiles et le shell. Ce sont ceux que tout éditeur rencontre quel que soit son langage — la configuration d'un projet est en TOML ou en YAML, sa documentation en Markdown, ses scripts en shell, la construction de son image dans un Dockerfile.

Golo n'en fait pas partie, ni Go, ni Rust, ni Python, ni MoonBit. Le langage qui définit un éditeur est enregistré par cet éditeur, et c'est pourquoi un fichier .mbt s'ouvre en texte brut ici et un fichier .golo s'ouvre en texte brut dans Turbo MoonBit.

Cela aurait pu aller dans l'autre sens. Mettre les six scanners dans la bibliothèque permettrait à n'importe quel éditeur de colorer n'importe lequel de ces langages, sans coût en dépendances — un scanner Golo est du Go ordinaire. L'idée a été rejetée parce que la bibliothèque grossirait d'un langage chaque fois que quelqu'un construit un éditeur, et parce que « qu'est-ce que cet éditeur enregistre ? » cesserait d'être la première question à poser sur un nouveau venu.

Pourquoi le scanner n'a pas été emprunté à GoloScript

GoloScript est écrit en Go, et son paquet lexer est exactement le tokeniseur que ce scanner réimplémente. Turbo Go se sert de go/scanner dans la même situation, la question est donc légitime.

La réponse tient au nom du module. Le go.mod de GoloScript déclare module golo, un nom nu sans hôte, et le système de modules de Go ne sait pas aller chercher un module portant un tel chemin où que ce soit : importer golo/lexer depuis un autre module exige une directive replace pointant vers un dépôt cloné à côté, et un replace committé est précisément ce que 01-release.tag.sh refuse de publier. Les règles du lexer sont donc reportées ici plutôt qu'appelées — et lexer/lexer.go et token/token.go sont la spécification contre laquelle le scanner est écrit, ligne pour ligne là où cela compte. La page sur la coloration nomme les endroits où cette spécification et le parseur de l'interpréteur ne sont pas d'accord.

Pourquoi le menu de l'outillage s'appelle ~G~olo et non golo, gogolo ou wagolo

La touche chaude était la partie facile. Neuf lettres sont prises par les menus fixes — F, E, S, R, C, O, W, N et H — et G n'en fait pas partie, elle tombe donc sur la première lettre du mot, ce qui ne coûte à personne un second regard. Turbo Rust n'a pas eu cette chance et a fini sur Rus~t~.

Le nom était la vraie décision, et elle a été prise comme chez tous les frères. Le menu contient ce que le projet a mis dans son fichier d'outils, et ce n'est pas toujours l'interpréteur : GoloScript lui-même, c'est trois binaires — golo, gogolo, wagolo — et le premier fichier d'outils qu'on écrit dépasse les trois, parce que les commandes d'un projet comprennent des conteneurs, des bases de données et une cible de Makefile ajoutée en 2019. Un menu appelé golo qui contient wagolo build est déjà un petit mensonge, et un qui contient docker compose up en est un gros. Golo est le langage, et le langage est ce pour quoi cet éditeur existe.

Pourquoi les tests pilotent le vrai éditeur

internal/gololang/editor_test.go construit un Turbo Golo entier sur un terminal simulé — app.New(screen, "turbo-classic", gololang.Profile()) — ouvre un fichier et vérifie la coloration, la barre de menus et les touches chaudes. Il n'utilise que l'API publique de la bibliothèque.

C'est délibéré. La suite de la bibliothèque prouve que la bibliothèque fonctionne ; ce que ces tests prouvent, c'est que cet éditeur est correctement assemblé — que Register a été appelé, que le profil a atteint la barre de menus, qu'un fichier .golo sort coloré et qu'un script avec un shebang golo aussi. Un bogue où main oublierait d'enregistrer Golo passerait tous les tests de turbo-core.

Le même fichier pilote un vrai golo lsp de bout en bout, dix fois. Il écrit un script, l'ouvre, démarre le serveur, puis :

  • tape une déclaration de fonction qui n'existe que dans le tampon, puis tape ses premières lettres sur une autre ligne et demande une complétion — golo lsp propose les mots-clés et les builtins pour n'importe quel fichier, une complétion contenant println ne prouverait donc rien ; une qui contient une fonction absente du disque prouve que le tampon a été envoyé ;
  • demande la complétion d'un préfixe vide et compare la réponse aux tables du scanner dans les deux sens — chaque mot-clé et chaque builtin que le scanner colore doit être proposé par le serveur, et tout ce que le serveur propose doit être connu du scanner, ce qui tient la table de 157 builtins à l'interpréteur plutôt qu'à la mémoire ;
  • demande la définition d'un appel et le survol de celui-ci, qui revient avec le commentaire # écrit au-dessus de la déclaration ;
  • demande les symboles du fichier ;
  • demande les références d'un appel — la déclaration et chaque appel dans le fichier — et son implémentation, qui est la déclaration elle-même, Golo n'ayant pas d'interfaces ;
  • cherche un symbole dans le projet, y compris dans un fichier que l'éditeur n'a jamais ouvert ;
  • ouvre un fichier qui ne parse pas et attend qu'un diagnostic arrive sans qu'on le demande — la seule fonctionnalité dont l'échec ressemble exactement au succès, parce qu'un éditeur qui n'a aucune erreur à montrer et un éditeur qui ne trouve pas l'erreur ont la même gouttière vide ;
  • ouvre un fichier contenant un commentaire // à la C et attend le lint qui dit que Golo utilise #.

Un dernier test épingle ce que golo lsp ne sait toujours pas faire : il n'annonce pas typeDefinition, la documentation le dit, et le test échoue si un futur golo se met à répondre — la page est alors revue plutôt que de vieillir en silence. C'est ainsi que les trois tests qui le précèdent sont nés : jusqu'à GoloScript v0.2.0 le même test épinglait les références, les implémentations et les symboles du projet comme refus, et il est passé au rouge le jour où le serveur les a appris.

Alternatives rejetées

Forker Turbo MoonBit. La façon évidente d'obtenir un cinquième éditeur, et la raison pour laquelle la bibliothèque existe à la place : cinq copies de onze mille lignes divergent en un mois, et chaque correction doit être faite cinq fois par quelqu'un qui se souvient qu'il y en a cinq.

Un système de plugins. Turbo Golo est un programme Go qui importe une bibliothèque. Il n'y a ni chargement dynamique ni ABI. En ajouter un reviendrait à figer l'API de chaque paquet de turbo-core plutôt que celle des quelques-uns qu'un profil touche.

Un fichier de configuration au lieu d'un profil. Le profil aurait pu être du TOML lu au démarrage, ce qui ferait d'un nouvel éditeur un fichier plutôt qu'un programme. Cela rendrait aussi le scanner inexprimable, et un éditeur à moitié configurable — tout sauf la coloration — est pire que l'une ou l'autre des réponses entières.

Importer le lexer de GoloScript. Pesé plus haut : le module ne peut pas être récupéré, et un replace ne peut pas être publié.

Liens avec le reste

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
# Architecture — explication

## De quoi s'agit-il ?

Turbo Golo, c'est une commande, un profil et un scanner. Tout le reste — le widget d'édition, les fenêtres, les menus, les dialogues, les thèmes, l'émulateur de terminal, l'arbre de fichiers, le client LSP — c'est [turbo-core](https://rickub.com/turbo-editors/turbo-core), la bibliothèque sur laquelle tous les éditeurs Turbo sont construits.

Cette page parle de cette séparation : ce qui est ici, ce qui est là-bas, et pourquoi la frontière passe où elle passe.

## Ce que contient ce dépôt

```
main.go                  les options, le terminal et le câblage
internal/gololang        tout ce qui fait de cet éditeur Turbo Golo
  gololang.go            le profil : nom, menu, serveur, où golo est installé
  scan.go                le répartiteur du scanner, les commentaires, ce qui franchit une ligne
  literals.go            les trois formes entre guillemets — "…", """…""" et '…'
  words.go               les nombres, les mots-clés, les 157 builtins, les conventions de nommage
  templates.go           trois déclarations //go:embed
  *.toml.tmpl            les trois fichiers de départ d'un projet, embarqués
```

Un millier de lignes environ en comptant les commentaires, dont quelque six cents pour le scanner — moins de cinq cents lignes de code au compte de qlty, et un tiers de celles-ci est la table des builtins. Il n'y a pas d'`internal/app`, pas d'`internal/ui`, pas d'`internal/buffer` : ceux-là existent une fois, dans la bibliothèque, et les six éditeurs les utilisent tels quels.

## Ce que fait `main`

Six choses, dans cet ordre :

1. Il lit les options.
2. Il appelle `gololang.Register()`, qui apprend à la bibliothèque à colorer les fichiers `.golo` et les scripts dont la première ligne nomme `golo`.
3. Il construit `gololang.Profile()` — la valeur qui dit que cet éditeur est Turbo Golo.
4. Il lit `.turbo-golo/settings.toml` dans le répertoire courant, s'il existe.
5. Il ouvre le terminal et confie l'écran, le nom du thème et le profil à `app.New`.
6. Il démarre `golo lsp` dans le répertoire du fichier édité, et lance la boucle d'événements.

C'est toute la commande. Chaque décision qu'elle prend — quel thème l'emporte, quels fichiers ouvrir, faut-il démarrer un serveur de langage — porte sur *cette exécution*, pas sur Golo.

## Le profil est la couture

```go
profile.Profile{
	Name:        "Turbo Golo",
	Slug:        "turbo-golo",
	Language:    "Golo",
	ToolsMenu:   "~G~olo",
	RootMarkers: nil,
	Server:      profile.Server{Command: "golo", Args: []string{"lsp"}, },
	Templates:   profile.Templates{Settings: , Snippets: , Tools: },
}
```

Tout ce qui serait sinon un `"turbo-golo"`, un `"golo"` ou un `".golo"` codé en dur quelque part dans onze mille lignes est un champ ici. La bibliothèque les lit ; rien dans la bibliothèque ne sait ce qu'ils signifient.

`Slug` porte plus qu'il n'y paraît. Le binaire s'appelle `turbo-golo`, le répertoire de projet `.turbo-golo`, la configuration personnelle vit dans `~/.config/turbo-golo`, et les variables d'environnement qui la remplacent sont `TURBO_GOLO_THEME_DIR` et `TURBO_GOLO_SNIPPET_DIR` — toutes dérivées de ce seul mot.

`RootMarkers` est le seul champ vide ici et rempli chez tous les frères. Go a `go.mod`, Rust `Cargo.toml`, Python `pyproject.toml`, MoonBit `moon.mod` ; Golo n'a aucun manifeste. Un script est un fichier et un programme est un répertoire de fichiers, il n'y a donc rien vers quoi remonter, et le `ProjectRoot` de la bibliothèque — sans marqueur — répond par le répertoire du fichier. C'est aussi tout ce dont `golo lsp` a besoin : il répond à propos du fichier qu'on lui donne et résout les imports depuis les modules embarqués dans le binaire, jamais depuis le disque.

## Pourquoi le scanner est ici et pas dans la bibliothèque

turbo-core colore huit langages lui-même : TOML, YAML, Markdown, JavaScript, HTML, XML, les Dockerfiles et le shell. Ce sont ceux que tout éditeur rencontre quel que soit son langage — la configuration d'un projet est en TOML ou en YAML, sa documentation en Markdown, ses scripts en shell, la construction de son image dans un Dockerfile.

Golo n'en fait pas partie, ni Go, ni Rust, ni Python, ni MoonBit. Le langage qui *définit* un éditeur est enregistré par cet éditeur, et c'est pourquoi un fichier `.mbt` s'ouvre en texte brut ici et un fichier `.golo` s'ouvre en texte brut dans Turbo MoonBit.

Cela aurait pu aller dans l'autre sens. Mettre les six scanners dans la bibliothèque permettrait à n'importe quel éditeur de colorer n'importe lequel de ces langages, sans coût en dépendances — un scanner Golo est du Go ordinaire. L'idée a été rejetée parce que la bibliothèque grossirait d'un langage chaque fois que quelqu'un construit un éditeur, et parce que « qu'est-ce que cet éditeur enregistre ? » cesserait d'être la première question à poser sur un nouveau venu.

## Pourquoi le scanner n'a pas été emprunté à GoloScript

GoloScript est écrit en Go, et son paquet `lexer` est exactement le tokeniseur que ce scanner réimplémente. Turbo Go se sert de `go/scanner` dans la même situation, la question est donc légitime.

La réponse tient au nom du module. Le `go.mod` de GoloScript déclare `module golo`, un nom nu sans hôte, et le système de modules de Go ne sait pas aller chercher un module portant un tel chemin où que ce soit : importer `golo/lexer` depuis un autre module exige une directive `replace` pointant vers un dépôt cloné à côté, et un `replace` committé est précisément ce que `01-release.tag.sh` refuse de publier. Les règles du lexer sont donc reportées ici plutôt qu'appelées — et `lexer/lexer.go` et `token/token.go` sont la spécification contre laquelle le scanner est écrit, ligne pour ligne là où cela compte. La [page sur la coloration](colouring-and-completion.md) nomme les endroits où cette spécification et le parseur de l'interpréteur ne sont pas d'accord.

## Pourquoi le menu de l'outillage s'appelle `~G~olo` et non `golo`, `gogolo` ou `wagolo`

La touche chaude était la partie facile. Neuf lettres sont prises par les menus fixes — F, E, S, R, C, O, W, N et H — et `G` n'en fait pas partie, elle tombe donc sur la première lettre du mot, ce qui ne coûte à personne un second regard. Turbo Rust n'a pas eu cette chance et a fini sur `Rus~t~`.

Le nom était la vraie décision, et elle a été prise comme chez tous les frères. Le menu contient ce que le projet a mis dans son fichier d'outils, et ce n'est pas toujours l'interpréteur : GoloScript lui-même, c'est trois binaires — `golo`, `gogolo`, `wagolo` — et le premier fichier d'outils qu'on écrit dépasse les trois, parce que les commandes d'un projet comprennent des conteneurs, des bases de données et une cible de `Makefile` ajoutée en 2019. Un menu appelé **golo** qui contient `wagolo build` est déjà un petit mensonge, et un qui contient `docker compose up` en est un gros. `Golo` est le langage, et le langage est ce pour quoi cet éditeur existe.

## Pourquoi les tests pilotent le vrai éditeur

`internal/gololang/editor_test.go` construit un Turbo Golo entier sur un terminal simulé — `app.New(screen, "turbo-classic", gololang.Profile())` — ouvre un fichier et vérifie la coloration, la barre de menus et les touches chaudes. Il n'utilise que l'API publique de la bibliothèque.

C'est délibéré. La suite de la bibliothèque prouve que la bibliothèque fonctionne ; ce que ces tests prouvent, c'est que *cet éditeur est correctement assemblé* — que `Register` a été appelé, que le profil a atteint la barre de menus, qu'un fichier `.golo` sort coloré et qu'un script avec un shebang `golo` aussi. Un bogue où `main` oublierait d'enregistrer Golo passerait tous les tests de turbo-core.

Le même fichier pilote un **vrai `golo lsp`** de bout en bout, dix fois. Il écrit un script, l'ouvre, démarre le serveur, puis :

- **tape une déclaration de fonction qui n'existe que dans le tampon**, puis tape ses premières lettres sur une autre ligne et demande une complétion — `golo lsp` propose les mots-clés et les builtins pour n'importe quel fichier, une complétion contenant `println` ne prouverait donc rien ; une qui contient une fonction absente du disque prouve que le tampon a été envoyé ;
- demande la **complétion d'un préfixe vide** et compare la réponse aux tables du scanner dans les deux sens — chaque mot-clé et chaque builtin que le scanner colore doit être proposé par le serveur, et tout ce que le serveur propose doit être connu du scanner, ce qui tient la table de 157 builtins à l'interpréteur plutôt qu'à la mémoire ;
- demande la **définition** d'un appel et le **survol** de celui-ci, qui revient avec le commentaire `#` écrit au-dessus de la déclaration ;
- demande les **symboles du fichier** ;
- demande les **références** d'un appel — la déclaration et chaque appel dans le fichier — et son **implémentation**, qui est la déclaration elle-même, Golo n'ayant pas d'interfaces ;
- cherche un **symbole dans le projet**, y compris dans un fichier que l'éditeur n'a jamais ouvert ;
- ouvre un fichier qui **ne parse pas** et attend qu'un diagnostic arrive sans qu'on le demande — la seule fonctionnalité dont l'échec ressemble exactement au succès, parce qu'un éditeur qui n'a aucune erreur à montrer et un éditeur qui ne trouve pas l'erreur ont la même gouttière vide ;
- ouvre un fichier contenant un **commentaire `//` à la C** et attend le lint qui dit que Golo utilise `#`.

Un dernier test épingle ce que `golo lsp` *ne sait toujours pas* faire : il n'annonce pas `typeDefinition`, la documentation le dit, et le test échoue si un futur golo se met à répondre — la page est alors revue plutôt que de vieillir en silence. C'est ainsi que les trois tests qui le précèdent sont nés : jusqu'à GoloScript v0.2.0 le même test épinglait les références, les implémentations et les symboles du projet comme refus, et il est passé au rouge le jour où le serveur les a appris.

## Alternatives rejetées

**Forker Turbo MoonBit.** La façon évidente d'obtenir un cinquième éditeur, et la raison pour laquelle la bibliothèque existe à la place : cinq copies de onze mille lignes divergent en un mois, et chaque correction doit être faite cinq fois par quelqu'un qui se souvient qu'il y en a cinq.

**Un système de plugins.** Turbo Golo est un programme Go qui importe une bibliothèque. Il n'y a ni chargement dynamique ni ABI. En ajouter un reviendrait à figer l'API de chaque paquet de turbo-core plutôt que celle des quelques-uns qu'un profil touche.

**Un fichier de configuration au lieu d'un profil.** Le profil aurait pu être du TOML lu au démarrage, ce qui ferait d'un nouvel éditeur un fichier plutôt qu'un programme. Cela rendrait aussi le scanner inexprimable, et un éditeur à moitié configurable — tout sauf la coloration — est pire que l'une ou l'autre des réponses entières.

**Importer le lexer de GoloScript.** Pesé plus haut : le module ne peut pas être récupéré, et un `replace` ne peut pas être publié.

## Liens avec le reste

- Ce que fait chaque paquet de la bibliothèque : [la référence des paquets de turbo-core](https://rickub.com/turbo-editors/turbo-core/blob/main/docs/fr/reference/packages.md)
- Comment la coloration fonctionne ici : [Coloration et complétion](colouring-and-completion.md)
- Pourquoi le menu des outils est une donnée : [Outils Golo](golo-tools.md)
- Les décisions qui ont survécu au refactoring : [Décisions de conception](design-decisions.md)