turbo-editors/turbo-gopublic Fork 0
v1.0.2
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-go.git
git clone ssh://git@rickub.com/turbo-editors/turbo-go.git

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

design-decisions.md · 109 lines · 11.8 KBmarkdown Blame HistoryRaw
📦 Turbo Go 3d7798b k33g 13h ago1# Décisions de conception — explication
2
3## De quoi s'agit-il ?
4
5Les choix qui ont façonné Turbo Go, quelles étaient les alternatives, et pourquoi elles ont été écartées. C'est la page à lire avant de modifier quelque chose qui paraît arbitraire.
6
7## Deux dépendances, pas une de plus
8
9Turbo Go dépend de `tcell/v2` et de `BurntSushi/toml`. Tout le reste est la bibliothèque standard — y compris le tokeniseur, le client JSON-RPC, le cadrage LSP et la gestion des fichiers.
10
11**Ce qui a été écarté.** `go.lsp.dev/jsonrpc2` aurait économisé peut-être trois cents lignes de turbo-core's `lsp`. `rivo/tview` en aurait économisé bien davantage dans turbo-core's `ui`. Une bibliothèque de coloration syntaxique aurait apporté cinquante langages au lieu d'un.
12
13**Pourquoi.** Un éditeur est un programme qu'on garde des années et qu'on modifie souvent. Chaque dépendance en est un morceau qu'on ne peut pas modifier, pas tester entièrement, et qu'il faut suivre. Le protocole est assez simple pour être écrit, et l'avoir écrit a placé toute la conversation à un endroit qu'un lecteur peut suivre. Trois cents lignes qu'on comprend valent mieux que trois cents qu'on hérite.
14
15L'exception confirme la règle : `tcell` n'est pas une commodité, c'est la base de compatibilité des terminaux, et la réimplémenter ne serait ni un petit travail ni un travail honnête.
16
17## Le framework de widgets est écrit à la main
18
19`tview` a des widgets. `bubbletea` a une architecture. Aucun des deux n'a ce qu'avait Turbo Vision : des fenêtres déplaçables qui se recouvrent avec des ombres, une barre de menus à lettres d'accès et des dialogues modaux, le tout dessiné en caractères semi-graphiques sur seize couleurs.
20
21L'architecture à la Elm de `bubbletea` redessine toute la vue à chaque message. Ce modèle est excellent pour un formulaire et malcommode pour un éditeur plein écran où les fenêtres s'empilent et où le curseur doit se trouver dans une cellule précise.
22
23Écrire le framework a coûté environ mille cinq cents lignes. En échange, l'éditeur ressemble à Turbo C plutôt qu'à une interface moderne portant un fond bleu, et chaque décision d'affichage se trouve à un fichier de distance.
24
25## Les rectangles sont en coordonnées écran absolues
26
27Le `Bounds()` de chaque widget indique où il se trouve réellement sur le terminal, pas où il se trouve par rapport à son parent. Tester si un clic l'atteint est alors un simple test de rectangle, et aucun événement n'a jamais besoin d'être traduit en descendant.
28
29**Le coût** est que les conteneurs placent leurs enfants dans l'espace de l'écran. **L'alternative** — des coordonnées relatives avec une traduction à chaque saut — déplace le calcul de la mise en page vers la gestion des événements, où il est fait bien plus souvent et où il est bien plus facile de se tromper. Le découpage se compose quand même correctement, puisqu'un peintre intersecte le découpage de son parent : un enfant dont le calcul est faux ne dessine rien plutôt que de dessiner par-dessus ses voisins.
30
31## Toute modification passe par une seule fonction
32
33`buffer.ReplaceRange` est le seul endroit où le texte est modifié. Insertion, retour arrière, suppression, indentation, collage et annulation y convergent tous, et c'est le seul endroit où sont maintenus l'historique d'annulation, le drapeau « modifié », le compteur de révision et le curseur.
34
35L'alternative — chaque opération tenant sa propre comptabilité — est la façon dont naissent les bugs d'annulation. Il y a exactement une chose à réussir, et elle est testée directement.
36
37## Les fenêtres suivent le terminal, elles ne s'y mettent pas à l'échelle
38
39Une fenêtre a un **mode de croissance**, qui nomme les bords du bureau qu'elle suit. Une fenêtre de document suit les bords droit et bas : son coin supérieur gauche reste où il est, et son coin opposé se déplace exactement autant que celui du terminal. Une fenêtre qui remplissait le terminal le remplit donc toujours, et une fenêtre que vous aviez décalée garde son décalage.
40
41**L'alternative était la mise à l'échelle proportionnelle** — multiplier le rectangle de chaque fenêtre par le rapport des tailles. Elle a été écartée parce qu'elle déplace des fenêtres que l'utilisateur a placées exprès, et parce que les arrondis la rendent destructive : réduisez puis agrandissez, et plus rien n'est où il était. Turbo Vision utilisait des modes de croissance, et c'est toujours la bonne réponse.
42
43Quel que soit son mode, une fenêtre est ensuite bornée à la taille du bureau. Une fenêtre plus grande que le bureau qui la contient a des parties que personne ne peut atteindre.
44
45
46## Les cases d'une fenêtre disent ce qu'elles vont faire, pas ce que la fenêtre est
47
48Le cadre porte deux cases : `[x]` à gauche ferme la fenêtre, `[■]` à droite lui donne tout le bureau.
49
50La case de fermeture était `[■]` — celle de Turbo Vision — et il fallait qu'elle bouge. Deux cases sur un même cadre doivent se distinguer d'un coup d'œil, et un bloc plein se lit bien plus volontiers « remplir l'écran » que « fermer ». `[x]` veut dire fermer depuis trente ans ; le bloc est allé au travail auquel il ressemble.
51
52La case d'agrandissement **change avec l'état de la fenêtre** : `[■]` tant qu'il reste de la place, `[▬]` une fois que la fenêtre remplit le bureau. L'autre solution était un symbole fixe, et elle rend le bouton ambigu précisément au moment où l'on en a besoin : on voit bien que la fenêtre est grande, mais pas si l'actionner va l'agrandir encore ou la remettre en place. Un contrôle qui montre son *état* laisse déduire l'action ; un contrôle qui montre son *action*, non.
53
54Une fenêtre qui n'a nulle part où s'agrandir n'affiche **aucune case**, plutôt qu'une case sans effet. Seul le bureau sait quelle surface une fenêtre remplirait : une fenêtre qui n'est sur aucun bureau n'a rien à proposer.
55
56**Window ▸ Maximise est le même bascule**, pas une action à sens unique. Un menu et un bouton en désaccord sur le sens d'« agrandir » seraient un bug qu'on signale, pas une subtilité qu'on apprécie.
57
58## L'annulation fusionne les séries de frappe
59
60Taper `func` puis Ctrl-Z retire les quatre lettres. Une série de retours arrière aussi. Déplacer le curseur clôt la série, et la frappe ne fusionne jamais avec l'effacement.
61
62L'annulation caractère par caractère est ce que donne une implémentation naïve, et c'est ce que faisait Turbo C lui-même. C'est aussi ce dont plus personne ne veut.
63
64## Les thèmes sont en TOML, avec deux formes d'héritage
65
66**Entre fichiers**, `inherits` prend les styles résolus du parent comme point de départ. Un thème à vous peut donc tenir en cinq lignes.
67
68**Entre clés**, le long des points : `syntax.keyword` retombe sur `syntax`, et `syntax` sur `default`. Cela se produit deux fois — une fois à l'analyse, pour qu'une entrée ne définissant que `fg` hérite de son `bg`, et une fois à la lecture, pour qu'un thème qui ne mentionne jamais `syntax.keyword` colore quand même les mots-clés.
69
70C'est cette seconde forme qui fait qu'un thème partiel est un thème utilisable, et c'est pourquoi il n'existe pas de thème laissant la moitié de l'écran non peinte.
71
72**Pourquoi TOML plutôt que JSON.** Les commentaires. Un thème est un fichier que l'on modifie à la main et que l'on annote.
73
74**Une couleur inconnue est une erreur**, pas un repli silencieux sur la couleur par défaut du terminal. Une faute de frappe qui repeint discrètement la moitié de l'écran est bien plus difficile à trouver qu'une qui le dit au chargement.
75
76## Le serveur de langage est optionnel par construction
77
78`app.Language` enveloppe toute la conversation avec gopls, et lorsqu'il n'y a pas de serveur, chaque méthode ne fait rien plutôt que d'échouer. Rien d'autre dans l'éditeur ne se demande si un serveur de langage existe.
79
80L'alternative — vérifier `nil` à chacun des vingt points d'appel — offre vingt occasions d'oublier. Ici, oublier est impossible : il n'y a rien à vérifier.
81
82C'est pourquoi `gopls` n'est ni embarqué, ni téléchargé, ni requis. Il est cherché dans le `PATH` et dans `GOPATH/bin`, et son absence est signalée sur la barre d'état avec l'unique commande qui la corrige.
83
84## L'enregistrement est atomique, et fidèle à l'octet près
85
86Un enregistrement écrit dans un fichier temporaire du même répertoire puis le renomme sur la cible, en conservant les permissions d'origine. Un enregistrement interrompu ne peut pas laisser un fichier source à moitié écrit.
87
88Par ailleurs, les fins de ligne avec lesquelles un fichier a été lu et son saut de ligne final — ou son absence — sont mémorisés : ouvrir puis enregistrer un fichier non modifié le reproduit octet pour octet. Un éditeur qui normalise silencieusement les fins de ligne transforme une modification d'une ligne en un diff du fichier entier.
89
90## Le presse-papier est celui de l'éditeur
91
92Un programme en terminal ne peut pas lire le presse-papier du système de façon portable. Plutôt que de faire semblant, Turbo Go partage un presse-papier entre ses propres fenêtres, ce que faisait Turbo C.
93
94## La version est une propriété du build, pas des sources
95
96La version était autrefois `const Version = "0.1.0"` dans a subdirectory. Elle était juste le jour où elle a été écrite et fausse pendant les quatorze commits suivants, parce que rien dans le fait de valider, taguer ou installer ne touche à une constante Go. Une boîte About, c'est ce que l'on regarde au moment de signaler un bug ; un numéro qui y nomme une release que le binaire n'est pas est pire que pas de numéro, parce qu'on le croit.
97
98Le numéro est donc pris au build. L'éditeur de liens estampille `git describe --tags --dirty` dans turbo-core's `version` package depuis le Makefile et depuis l'installeur, ce qui fait que `make install` produit un éditeur qui nomme le commit dont il vient. Quand rien ne l'a estampillé, le binaire interroge `runtime/debug.ReadBuildInfo()`, qui couvre le seul chemin impossible à estampiller : `go install rickub.com/turbo-editors/turbo-go@v0.2.0`, où aucun Makefile n'intervient et où l'outil Go connaît la version du module. Ce n'est que si les deux se taisent qu'il annonce `unknown` — délibérément pas un numéro, puisque l'échec contre lequel tout ceci est conçu est justement une version vraisemblable que personne n'a posée.
99
100Deux limites du système de build expliquent le reste de la conception. **Il ne lit pas les tags git**, donc un `go build .` nu ne pourra jamais annoncer `0.1.0-14-g88a4c38`, si astucieux que soit le code ; il annonce `devel` plus le commit, et la documentation le dit plutôt que de laisser croire que tous les builds se valent. Et ce qu'il annonce *effectivement* pour un tel build est une **pseudo-version**`v0.1.1-0.20260831165958-88a4c3859bf3` — affichée comme `devel` à la place, parce que son `0.1.1` est un correctif qui n'existe pas et serait lu comme tel.
101
102`vcs.time` est délibérément inutilisé. C'est l'horodatage du commit, et tout binaire est lié après le commit dont il provient : l'étiqueter « Built » serait faux sur chacun d'eux. Une date de build ne s'affiche que si un build en a réellement estampillé une, la même règle que suit la boîte About de bout en bout : **un fait que personne n'a enregistré n'a pas de ligne**, plutôt qu'une ligne vide qui se lit comme un échec à la remplir.
103
104Rejeté : une cible `make release` qui tague, construit et pousse. Publier tient en trois commandes git, et les emballer masque laquelle a échoué ; l'estampillage de la version était la partie qu'on ne pouvait pas faire à la main de façon fiable, et c'est celle qui a été automatisée.
105
106## Liens avec le reste
107
108- Ce que sont les paquets et comment ils s'articulent : [Architecture](architecture.md)
109- Comment fonctionnent la coloration et la complétion : [Coloration et complétion](colouring-and-completion.md)