Architecture — explication
De quoi s'agit-il ?
turbo-core est un IDE de terminal avec un trou à l'endroit du langage. Dix-huit paquets mettent en œuvre tout ce que fait un éditeur — texte, annulation, fenêtres, menus, dialogues, thèmes, un émulateur VT, une arborescence de fichiers, un client LSP, un agent de code — et aucun d'eux ne sait quel langage est édité. Un éditeur bâti dessus, c'est une commande, un profile.Profile, et un analyseur.
La bibliothèque n'a pas été conçue ainsi. Elle a été extraite de Turbo Go, qui avait été un unique programme de 35 000 lignes pendant quinze jours, au moment où l'on a voulu un second éditeur. La quasi-totalité a bougé sans changement ; la couture est là où il s'est avéré qu'elle devait être, pas là où quelqu'un l'avait prédit.
La règle qui maintient la forme
Les paquets qui contiennent la logique intéressante ignorent qu'un terminal existe.
buffer, jsonrpc, lsp, syntax et profile se testent en appelant des fonctions et en comparant des valeurs. acp se coupe de la même façon : son client de protocole et sa Transcript prennent des valeurs et en rendent, et seule sa View a besoin d'un écran. ui, editor, filetree et app se testent à travers le SimulationScreen de tcell. terminal se coupe de la même façon en interne : son analyseur et son écran prennent des octets et renvoient une grille sans aucun processus derrière, et c'est là que se trouvent la plupart de ses tests.
Les dépendances vont strictement vers le bas. Il n'y a pas de cycle, et — c'est la partie qui mérite d'être défendue — pas d'indirection par interface introduite pour en empêcher un. La communication vers le haut passe par des champs de fonction (OnChange, OnCursorMove, OnCompletionRequest) plutôt que par des interfaces, parce qu'un champ fait une ligne alors qu'une interface fait un type, une implémentation et un nom pour les deux.
Pourquoi le profil est une structure et non une interface
Le dessin évident pour « les parties qui diffèrent par éditeur » est une interface : type Editor interface { Name() string; Server() Server; … }. Il a été rejeté.
Une interface serait implémentée une fois par éditeur, ce qui fait un type et un jeu de méthodes qui ne font que renvoyer des constantes. Une structure est remplie une fois par éditeur, ce qui fait un littéral. La différence compte surtout pour le troisième éditeur, celui pour lequel tout l'exercice a été fait : remplir un littéral est quelque chose qu'on fait en lisant la référence, et implémenter une interface est quelque chose qu'on fait en lisant l'implémentation de quelqu'un d'autre.
La structure rend aussi visible en un seul endroit toute la différence. golang.Profile() fait soixante lignes et est la réponse complète à « qu'est-ce qui fait de ceci Turbo Go ? ».
Pourquoi l'analyseur du langage vit dehors
Huit langages sont colorés ici — TOML, YAML, Markdown, JavaScript, HTML, XML, les Dockerfiles et le shell — et le langage pour lequel l'éditeur existe ne l'est pas. Turbo Go enregistre Go ; Turbo Rust enregistre Rust ; ni l'un ni l'autre n'est connu de la bibliothèque.
Les huit qui sont restés sont ceux que tout éditeur rencontre quel que soit son objet : la configuration d'un projet est du TOML ou du YAML, sa documentation du Markdown, ses scripts du shell, sa construction d'image un Dockerfile. Celui qui est parti est celui qui définit l'éditeur. Que Turbo Rust colore Go ne serait pas nuisible à proprement parler — mais cela voudrait dire que la bibliothèque gagne un langage chaque fois que quelqu'un construit un éditeur, et que la première question à propos d'un nouvel éditeur cesserait d'être « qu'enregistre-t-il ? ».
Le coût, accepté : l'auteur d'un langage écrit contre une boîte à outils exportée plutôt que privée, donc LineScanner et ses onze méthodes sont de l'API publique et ne peuvent plus être remodelées librement. C'est une vraie contrainte, et c'est le prix d'une couture réelle.
Pourquoi le registre est un état de paquet
syntax.Register écrit dans une table au niveau du paquet. C'est de l'état global mutable, ce qui est d'ordinaire une mauvaise odeur.
C'est la forme que la bibliothèque standard donne au même problème — image.RegisterFormat, database/sql.Register — et les raisons se transposent : l'enregistrement a lieu une fois au démarrage avant que quoi que ce soit ne lise, rien ne retire jamais une entrée, et l'alternative serait de faire passer une valeur de registre à travers editor.View, syntax.Cache et tous les endroits qui demandent quel langage est un fichier.
Ce n'est pas sûr depuis deux goroutines à la fois, et il n'y a aucune raison que ça le soit. L'enregistrement a sa place dans main, à côté des drapeaux.
Pourquoi theme reçoit un répertoire alors que tout le reste reçoit un profil
settings, snippets, tools et lsp reçoivent tous un profile.Profile, parce que chacun en a besoin de plusieurs choses — un répertoire, un modèle, un nom de menu, une commande. theme reçoit un simple userDir string, parce qu'il n'a besoin que d'une seule chose et qu'un unique argument nommé dit laquelle.
Cela a l'air incohérent et c'est délibéré. Un paquet qui recevrait un profil dont il n'utilise qu'un champ serait un paquet qui dépend de toute l'identité de l'éditeur pour répondre à une question sur un répertoire.
Comment cela se relie au reste
Les éditeurs bâtis dessus sont la preuve que la couture est réelle. Les paquets propres à Turbo Go sont passés de quatorze à un ; Turbo Rust a été bâti sur la bibliothèque sans la modifier, et les quatre défauts apparus pendant sa construction étaient tous dans son propre analyseur plutôt que dans la bibliothèque — ce qui est le résultat pour lequel l'extraction a été faite. Turbo Python, le troisième, a été bâti sur un tag publié sans la moindre modification de la bibliothèque.
Voir aussi
- Ce que contient chaque paquet : référence des paquets
- Où tombe la frontière : ce qui appartient ici
Pourquoi jsonrpc a été extrait de lsp
La bibliothèque n'a longtemps parlé qu'un seul protocole JSON-RPC, et le code correspondant vivait dans lsp, où il avait sa place. Un second protocole — l'Agent Client Protocol, que acp parle à un agent de code — en a fait le mauvais endroit, et la différence entre les deux s'est révélée tenir à une seule chose : la façon dont un message est séparé du suivant. LSP met devant chacun un en-tête Content-Length à la mode HTTP ; ACP les sépare par un saut de ligne. Tout ce qui est au-dessus de cette couche est identique.
jsonrpc détient donc le protocole et prend un Framer ; lsp.Framing et acp.Framing en sont les deux implémentations, une dizaine de lignes chacune. Aucun des deux clients ne sait que l'autre existe.
L'extraction a changé une chose à la couche protocole, plutôt que de simplement la déplacer. Les questions d'un serveur de langage peuvent toutes recevoir leur réponse là où elles arrivent : il demande sa configuration au démarrage, et le client connaît déjà la réponse. Celles d'un agent, non — session/request_permission reçoit sa réponse d'une personne qui regarde une boîte de dialogue, et ouvrir une boîte appartient à la goroutine qui dessine. Un gestionnaire de requête reçoit donc désormais un *jsonrpc.Request et ne renvoie rien : il peut répondre sur-le-champ, comme le fait lsp, ou garder la requête et répondre plusieurs tours de boucle plus tard, depuis une autre goroutine.
C'est la quatrième fois que cette bibliothèque parvient à la même conclusion — l'enregistrement automatique, la ré-annonce au serveur de langage et les redessins du terminal sont les autres. PostEvent a le droit de jeter ce qui ne rentre pas : un événement peut donc provoquer un tour de boucle, mais ne doit jamais être le seul porteur d'un fait.
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 |
|