Coloration et complétion — explication
De quoi s'agit-il ?
Les deux fonctionnalités qui font de Turbo Go un éditeur pour Go plutôt qu'un éditeur de texte qui se trouve être écrit en Go : la coloration syntaxique et la complétion par un serveur de langage. Elles fonctionnent de façons très différentes, et cette différence est instructive.
Coloration : le tokeniseur du compilateur lui-même
Turbo Go n'a pas de définition de syntaxe. Il appelle go/scanner — l'analyseur lexical qu'utilise la chaîne d'outils Go elle-même — et transforme les jetons obtenus en intervalles colorés.
Cela signifie que les mots-clés, les littéraux et les opérateurs sont reconnus exactement comme le compilateur les reconnaît. Les chaînes brutes, l'insertion automatique de points-virgules, les séparateurs 0x_FF, tout. Il n'y a aucune expression régulière à se tromper subtilement, ni aucune table à mettre à jour quand le langage évolue.
La tolérance est tout l'enjeu
Le source sous un curseur est syntaxiquement invalide la plupart du temps pendant qu'on le tape. Une chaîne à moitié écrite, une accolade non fermée, un identifiant interrompu au milieu. Un coloriseur qui abandonne devant une entrée invalide est un coloriseur qui s'éteint précisément au moment où on le regarde.
L'analyseur tourne donc dans son mode le plus indulgent et toute erreur de syntaxe est écartée. Il rend malgré tout des jetons exploitables : une chaîne non terminée revient comme une chaîne allant jusqu'à la fin de la ligne, un /* non fermé comme un commentaire allant jusqu'à la fin du fichier. Ce qui est précisément le comportement souhaité — les couleurs restent stables, et elles indiquent ce qui ne va pas.
Ce que l'éditeur ajoute
Trois distinctions que l'analyseur ne fait pas, parce qu'elles relèvent de la lecture et non de l'analyse :
- un identifiant avant
(, ou aprèsfunc, est une fonction - un identifiant après
type,structouinterfaceest un type - parenthèses, virgules, points et points-virgules sont de la ponctuation, séparée des opérateurs qui calculent, pour qu'un thème puisse les atténuer
Les noms prédéclarés — int, error, nil, len, min — sont reconnus par leur nom, pas comme mots-clés, parce qu'ils n'en sont pas : un fichier peut les masquer, et les colorer quand même comme les prédéclarés est ce que font tous les autres éditeurs Go.
Pourquoi c'est assez rapide pour être fait naïvement
Analyser tout le fichier à chaque frappe paraît coûteux, et le serait. Cela n'arrive pas : le tampon tient un compteur de révision, incrémenté à chaque modification, et le coloriseur ne réanalyse que lorsque ce nombre a bougé. L'éditeur se redessine bien plus souvent que le texte ne change — chaque déplacement du curseur, chaque défilement — et tous ces redessins sont gratuits.
Le prix de ce choix
Un langage n'est coloré que si quelqu'un a écrit un scanner pour lui. Il n'existe aucun langage de définition dans lequel écrire une définition : chacun est du code Go.
Huit d'entre eux — TOML, YAML, Markdown, JavaScript, HTML, XML, les Dockerfiles et le shell — vivent dans turbo-core, parce que tout éditeur bâti dessus les rencontre quel que soit son langage. L'analyseur Go vit ici, dans internal/golang, et est enregistré au démarrage par syntax.Register. C'est pourquoi un fichier .rs s'ouvre en texte brut dans Turbo Go : cet éditeur enregistre Go et rien d'autre.
C'est une vraie limitation, et elle a été acceptée délibérément : ceci est un éditeur pour Go. Un coloriseur générique aurait apporté une dépendance, un format de définition, et un écart permanent entre « ce que le coloriseur croit que Go est » et ce que Go est.
Les huit autres langages
Le TOML est venu en premier, et il a mérité son scanner en étant incontournable : l'éditeur lit deux fichiers TOML — les fichiers de thème et le .turbo-go/settings.toml d'un projet — et tous deux sont faits pour être édités dans l'éditeur lui-même. Livrer un fichier de réglages plein de commentaires explicatifs puis l'afficher en gris uniforme aurait été curieux.
Markdown, JavaScript, HTML et shell ont suivi pour une raison plus simple : c'est ce qui accompagne le Go dans un projet Go. Un dépôt a un README.md, quelques scripts, et souvent une page ou un peu de JavaScript ; un éditeur qui ne colore que les fichiers .go vous fait le quitter pour tout le reste. YAML, XML et les Dockerfiles les ont rejoints pour le même motif : ce qu'un projet Go garde à côté de son code est désormais tout aussi souvent un fichier compose, un workflow d'intégration continue ou une construction d'image — et un fichier compose en gris uni est justement celui dont on veut le plus voir la forme. Ils sont désormais partagés : écrits une fois ici, et hérités par tout éditeur bâti sur turbo-core.
Chacun fait quelques centaines de lignes et ils partagent une petite mécanique commune — une ligne, une position dedans, et les spans trouvés jusque-là. Ce qu'ils ne partagent pas, c'est la moindre tentative de moteur général. Pas de langage de motifs, pas de format de grammaire, pas de table d'expressions régulières : chaque scanner est du Go ordinaire qu'un lecteur peut suivre, et en ajouter un sixième consiste à en écrire un sixième plutôt qu'à apprendre une notation.
Ils s'arrêtent à des endroits que la référence énonce franchement, et ces arrêts ont été choisis, non subis :
- Pas d'expressions régulières JavaScript. Distinguer
/x/gd'une division exige de savoir si le token précédent pouvait terminer une expression. Une mauvaise supposition colore le reste de la ligne comme une chaîne — un échec bien plus bruyant que de laisser une regex à la couleur d'un opérateur. - Pas de heredocs shell. Suivre
<<EOFjusqu'à son délimiteur signifie porter un mot arbitraire d'une ligne à l'autre, avec en plus les graphies<<-et à délimiteur cité, pour une construction qui est en général quelques lignes de texte brut. - Pas de JavaScript dans
<script>. Cela demande de suivre un élément sur plusieurs lignes et de remapper les colonnes d'un autre scanner — et le même argument réclamerait ensuite le CSS. - Pas de langage dans une clôture Markdown.
```goest d'une seule couleur. Le colorer correctement suppose que chaque scanner soit atteignable depuis tous les autres, ce qui est le début du moteur général que ce paquet n'a pas.
La règle qui les unit est qu'un scanner ne devine rien. Là où une construction ne peut être reconnue sans en savoir plus que ce qu'une ligne contient, elle est laissée telle quelle plutôt qu'approximée : un coloriseur qui se trompe est pire qu'un coloriseur discret.
Cinq classes dont Go n'a rien à dire
Les douze classes que produit le tokeniseur Go couvrent presque entièrement les huit autres langages — une chaîne est une chaîne dans tous. Cinq choses n'avaient pas de place : un titre Markdown, ses emphases et ses liens, une balise et un attribut HTML.
Réutiliser les classes existantes était l'option la moins chère, et c'est celle retenue pour le TOML, où un en-tête de table se lit réellement comme un type et une clé comme un identifiant. Elle ne tient pas pour les langages de balisage : un titre n'est pas un mot-clé, une balise non plus, et un thème qui voudrait des titres discrets et des mots-clés voyants ne pourrait pas le dire. D'où syntax.heading, syntax.tag, syntax.attribute, syntax.emphasis et syntax.link.
Le coût est réel et pèse sur les thèmes écrits ailleurs : un thème qui n'en définit aucune retombe le long des points sur syntax puis default, donc le Markdown reste lisible mais ses titres ne se distinguent pas. Chaque thème livré définit les cinq, et un test échoue si l'un cesse de le faire.
Complétion : le programme de quelqu'un d'autre
La complétion fonctionne à l'inverse. Turbo Go ne connaît rien au système de types de Go et n'essaie pas : il demande à gopls, via le Language Server Protocol, et dessine la réponse.
Optionnelle, et pas par hasard
L'éditeur est pleinement utilisable sans serveur de langage. Pas dégradé — le tampon, la coloration, les thèmes, les fenêtres, la recherche, tout fonctionne à l'identique. Seules la complétion, la description de symbole et le saut à la définition manquent, et la barre d'état le dit avec l'unique commande qui corrige la situation.
C'est garanti par construction plutôt que par discipline. Language enveloppe toute la conversation, et sans serveur chaque méthode ne fait strictement rien. Il n'y a aucun if server != nil ailleurs dans l'éditeur, parce qu'il n'y a rien à vérifier.
Le piège du protocole
Le protocole compte les colonnes en unités de code UTF-16. L'éditeur les compte en runes. En ASCII, les deux donnent le même nombre — ce qui est exactement pourquoi se tromper là-dessus survit aux tests, jusqu'à ce que quelqu'un ouvre un fichier comportant un accent dans un commentaire, et que chaque complétion en aval tombe une colonne à côté.
Chaque position qui franchit cette frontière est donc convertie, et la conversion est testée avec une clef de sol, qui nécessite une paire de substitution et compte donc pour deux.
L'autre piège
gopls pose des questions à son client. Au démarrage, il demande workspace/configuration — et attend la réponse. Un client qui ne fait qu'émettre des requêtes et lire des réponses ne termine jamais son initialisation, et se bloque sans la moindre erreur.
La connexion route donc les requêtes serveur → client vers un gestionnaire, et le client répond par une configuration vide, ce qui signifie « utilise tes valeurs par défaut ».
Toujours bornée
Chaque requête a une échéance : trois secondes pour une complétion, trente pour la poignée de main, car un gopls froid a un graphe de modules à charger avant de pouvoir dire bonjour. Un serveur qui cesse de répondre ralentit l'éditeur et ne l'arrête jamais.
Une complétion qui arrive après que vous ayez tapé trois caractères de plus n'est pas une complétion, c'est une interruption — c'est pourquoi la requête est faite de façon synchrone et abandonnée rapidement, plutôt que livrée en retard.
Neuf questions, une seule connexion
La complétion est la chose la plus bruyante que fasse le serveur de langage, et la moins instructive. La même connexion en répond huit autres, qui se répartissent en trois sortes selon ce qui revient.
Quelque chose à lire. hover — qu'est-ce que c'est ? — dessiné dans une boîte.
Des lieux dans le code. definition, typeDefinition, implementation, references. Une requête chacune, une seule forme de réponse pour toutes, ce qui explique qu'elles ne soient qu'une fonction en dessous. Un lieu unique est ouvert ; plusieurs sont proposés en liste, parce qu'une réponse unique est l'exception plutôt que la règle — une interface Go a autant de définitions qu'elle a d'implémentations, et cet éditeur a longtemps pris la première en jetant les autres.
Des noms. documentSymbol pour le plan d'un fichier, workspace/symbol pour une recherche dans tout le projet. Le protocole a trois formes pour un symbole et l'éditeur en veut une : l'aplatissement se fait donc là où les réponses arrivent, pas là où elles sont dessinées.
Et une chose que personne ne demande : publishDiagnostics arrive de lui-même, dès que le serveur a un avis, pour tous les fichiers qu'il a chargés — le plus souvent davantage que celui qu'on a devant soi. C'est pourquoi Problems liste tous les fichiers et pas seulement le courant, et pourquoi la marque dans la gouttière apparaît sans qu'on ait appuyé sur quoi que ce soit.
L'éditeur ne demande rien de tout cela avant que le serveur ne se dise prêt, et dit laquelle des deux situations s'applique quand une question reste sans réponse. « Rien trouvé » et « je n'ai pas fini de charger » sont la même réponse vide et une nouvelle très différente ; les confondre est la façon la plus déroutante dont la complétion ait jamais échoué ici, et les huit autres en auraient hérité gratuitement.
Deux fonctionnalités, deux formes
Il vaut la peine de remarquer pourquoi elles ont fini si différentes.
La coloration doit être instantanée et toujours assez juste, sur un texte le plus souvent invalide. Cela appelle une réponse locale, tolérante et bon marché — et le tokeniseur est déjà dans la bibliothèque standard.
La complétion doit être occasionnellement juste sur tout le programme, dépendances comprises. C'est le travail d'un compilateur, c'est coûteux, et c'est déjà résolu par un programme qui ne fait que cela.
La première méritait d'être écrite. La seconde méritait d'être demandée.
Liens avec le reste
- Où vivent ces deux mécanismes dans le code : Architecture
- La politique de dépendances qui a façonné les deux : Décisions de conception
- Faire fonctionner la complétion : Activer la complétion Go
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 112 113 114 115 116 117 118 119 120 121 122 |
|