| 📦 Turbo MoonBit cc1f595 k33g 22h ago | 1 | # Coloration et complétion — explication |
| 2 | |
| 3 | ## De quoi s'agit-il ? |
| 4 | |
| 5 | Les deux fonctions qui font de Turbo MoonBit un éditeur *pour MoonBit* plutôt qu'un éditeur de texte qui ouvre des fichiers `.mbt` : la coloration syntaxique, et la complétion venue d'un serveur de langage. Elles fonctionnent très différemment, et la différence est instructive. |
| 6 | |
| 7 | ## La coloration est à nous ; la complétion ne l'est pas |
| 8 | |
| 9 | La coloration se fait ici, en quelque six cents lignes de Go écrites à la main. La complétion est faite par moon-lsp, et Turbo MoonBit se contente de demander et de dessiner. |
| 10 | |
| 11 | Ce partage n'est pas un accident d'effort. La coloration doit être **instantanée et tolérante** : elle tourne à chaque frappe, sur du texte invalide la plupart du temps qu'on l'écrit, et un coloriseur qui s'arrête pour réfléchir ou qui abandonne devant du code cassé est pire que pas de coloriseur du tout. La complétion doit être **juste**, ce qui pour MoonBit veut dire suivre les imports, résoudre un nom à travers la hiérarchie de classes où il a été affecté, et lire la surface publique de chaque paquet installé — et rien de ce qui doit être instantané ne peut être cela aussi. |
| 12 | |
| 13 | L'éditeur dessine donc des couleurs qu'il a calculées lui-même, et montre des complétions calculées par quelqu'un d'autre. |
| 14 | |
| 15 | ## Pourquoi MoonBit est analysé à la main |
| 16 | |
| 17 | MoonBit n'a pas de lexeur disponible sous forme de paquet Go. Turbo Go peut passer par `go/scanner`, la bibliothèque standard analysant son propre langage ; Turbo MoonBit n'a rien de tel, et les trois voies possibles ont été pesées. |
| 18 | |
| 19 | **Faire tourner un vrai lexeur MoonBit** voudrait dire lancer `moonc` et lui demander des jetons — un processus par frappe, et une dépendance sur une chaîne d'outils que l'éditeur ne devrait pas exiger pour colorer un fichier. |
| 20 | |
| 21 | **Embarquer une grammaire** — tree-sitter ou équivalent — voudrait dire une bibliothèque native, une étape de compilation et un binaire qui ne se compile plus partout. Turbo Core tient à deux dépendances directes et demi ; ce n'est pas ici qu'on ajoute la troisième. |
| 22 | |
| 23 | **Écrire un scanner à la main** demande un fichier de plus et donne quelque chose qui tourne à chaque frappe sans rien allouer d'inattendu, ne casse jamais sur du texte invalide et se lit comme du Go ordinaire. |
| 24 | |
| 25 | Le scanner, donc. Quelque six cents lignes, un fichier chacun pour l'aiguillage, les littéraux et les mots — et aucune tentative de moteur généraliste. Pas de langage de motifs, pas de format de grammaire, pas de table d'expressions régulières : du Go ordinaire qu'un lecteur peut suivre, la règle même que suivent les huit scanners de turbo-core. |
| 26 | |
| 27 | ## Rien ne franchit une fin de ligne |
| 28 | |
| 29 | Tous les autres éditeurs de cette famille font passer un état réel à travers leur scanner. Turbo Go et Turbo Rust portent une profondeur de commentaire de bloc ; Turbo Rust porte en plus le délimiteur d'une chaîne brute ; Turbo Python porte lequel des deux guillemets a ouvert un littéral triple. Turbo MoonBit ne porte rien du tout, et c'est un fait sur le langage plutôt qu'un raccourci : |
| 30 | |
| 31 | - **Il n'y a pas de commentaire de bloc.** La grammaire le dit en toutes lettres : « MoonBit n'a pas de forme de commentaire de bloc. » `//` va jusqu'à la fin de la ligne, `///` est un commentaire de documentation qui fait de même. |
| 32 | - **Aucun littéral ne peut atteindre la ligne suivante.** Pour les chaînes, les octets, les expressions régulières, les caractères et les octets-caractères, « un saut de ligne avant le guillemet fermant signale un littéral de chaîne non terminé ». Une ligne qui se termine à l'intérieur d'un littéral est du source cassé, pas une construction. |
| 33 | - **Une chaîne multiligne n'est pas un littéral qui s'étend sur plusieurs lignes.** C'est une suite de lignes préfixées `#|` ou `$|`, chacune un jeton complet, que le compilateur joint ensuite par un saut de ligne. |
| 34 | - **Un attribut tient explicitement sur une ligne** : « tout ce qui va jusqu'au saut de ligne suivant est la charge utile brute ». |
| 35 | |
| 36 | Le type de report est donc vide, et c'est un type nommé plutôt qu'un `struct{}` écrit sur place, pour que le raisonnement ait un endroit où vivre. Si MoonBit acquiert un jour une construction qui franchit les lignes, c'est ce type qui gagnera un champ. |
| 37 | |
| 38 | Ce que cela achète mérite d'être dit clairement : **un guillemet égaré ne peut pas peindre le reste du fichier.** Dans tous les autres éditeurs d'ici, une chaîne non terminée est un cas que le scanner doit décider d'*abandonner*, et se tromper sur cette décision transforme une frappe en un écran entier de vert. Ici, il n'y a pas de décision à rater. |
| 39 | |
| 40 | ## Là où le scanner s'appuie sur le langage, et non sur une convention |
| 41 | |
| 42 | C'est ce qui rend le scanner de MoonBit plus court que celui de ses frères, et la raison tient en une règle lexicale. |
| 43 | |
| 44 | **Un nom capitalisé est un type, et c'est la règle du langage plutôt qu'une habitude.** La grammaire définit `uident` comme commençant « par une majuscule ASCII », et seuls un type, un trait ou un constructeur d'énumération peuvent s'écrire ainsi. Turbo Python doit consulter la PEP 8 pour distinguer `ValueError("non")` de `parse("non")` ; Turbo Rust doit tenir une table des constructeurs que le langage nomme, parce que `Some(x)` ressemble à un appel. Ici, la casse *est* la réponse : il n'y a donc aucune table des types intégrés dans ce dépôt — `Int`, `StringBuilder` et un type écrit ce matin sont colorés par la même ligne de code. |
| 45 | |
| 46 | **Ce que cela coûte est unique, et inévitable.** Un constructeur d'énumération à vous — `Circle(1.0)` — est coloré en type, parce que rien dans la syntaxe ne le sépare d'un type appliqué à des arguments. Inventer une séparation reviendrait à se tromper dans les deux sens au lieu d'un. |
| 47 | |
| 48 | **Le prélude, lui, est une table, et elle a été lue plutôt que retenue.** `println`, `abort`, `fail`, `ignore`, `inspect` et les autres proviennent du fichier d'interface généré de `moonbitlang/core/prelude`. Cela compte plus qu'il n'y paraît : une table écrite d'habitude aurait contenu `print`, et MoonBit n'a jamais eu de `print`. Les noms dépréciés du prélude — `dump`, `not`, `tap` — sont délibérément absents, parce que les colorer en primitives présenterait comme siennes quatre choses que le langage cherche à retirer. |
| 49 | |
| 50 | ## Le seul endroit où la grammaire doit être suivie à la lettre |
| 51 | |
| 52 | `1..=2`, c'est un entier et un opérateur d'intervalle. Un scanner qui avalerait n'importe quel point après un nombre lirait le double `1.` et laisserait `.=2` derrière lui, et tous les intervalles de tous les fichiers seraient mal colorés. |
| 53 | |
| 54 | La grammaire tranche en une phrase — « avant `..`, l'entier se termine d'abord, donc `1..=2` commence par `1` puis `..=` » — et le scanner la suit exactement : un point ne rejoint un nombre que si un second ne le suit pas. La même discipline gouverne les suffixes. `42UL` est un seul nombre et `42u` est `42` suivi du nom `u`, parce que la grammaire dit que les suffixes sont en majuscules, et colorer `42u` en littéral inventerait quelque chose que le compilateur s'apprête à rejeter. |
| 55 | |
| 56 | Un point a deux autres métiers, et tous deux ont dû être écrits explicitement plutôt que rangés dans la ponctuation. `pair.0` est un accès de tuple. `xs.length()` est une méthode — et le nom qui suit le point est cherché *sans* la table des mots-clés, parce que les identifiants pointés de MoonBit « suivent les règles de casse des identifiants sans consulter la table des mots-clés, si bien que `.if` est valide ». Un enregistrement avec un champ nommé `type` est du MoonBit ordinaire, et un scanner qui colorerait ce champ en mot-clé affirmerait quelque chose que le langage contredit. |
| 57 | |
| 58 | ## Ce que le scanner refuse de deviner |
| 59 | |
| 60 | Là où une construction ne peut pas être reconnue depuis ce que contient une seule ligne, elle est laissée tranquille plutôt qu'approximée. Un coloriseur qui se trompe est pire qu'un coloriseur discret : |
| 61 | |
| 62 | | Non reconnu | Parce que | |
| 63 | | --- | --- | |
| 64 | | L'expression à l'intérieur de `\{…}` | La grammaire la fait aller jusqu'à « l'accolade correspondante », celles des littéraux imbriqués ne comptant pas : trouver la fin demande l'analyseur syntaxique. Un compteur d'accolades qui se tromperait terminerait la chaîne trop tôt, et un littéral qui avale le reste de la ligne est la façon la plus bruyante dont un coloriseur puisse casser. Une seule étendue plate est la réponse honnête pour le cas ordinaire, et c'est celle que Turbo Python donne à une f-string pour la même raison. Sa limite est une *chaîne* imbriquée dans l'interpolation — voir plus bas | |
| 65 | | Un mot réservé comme mot-clé | `move`, `ref`, `static`, `unsafe`, `await` et quarante autres sont *réservés* plutôt que mots-clés : le lexeur les traite comme des identifiants et se contente d'avertir. Les colorer dirait au lecteur qu'il ne peut pas écrire `let ref = 1` alors qu'il le peut | |
| 66 | | Un identifiant contenant des lettres non ASCII | MoonBit accepte le CJK et plusieurs autres plages Unicode dans un nom. Les prédicats de caractères sur lesquels ce scanner est bâti sont ASCII : un tel nom est franchi sans couleur plutôt que deviné — une frontière qu'il vaut mieux connaître qu'un défaut à cacher | |
| 67 | | `moon.mod`, `moon.pkg` et `moon.work` | Ce sont les fichiers du DSL de configuration de MoonBit plutôt que du MoonBit. Les colorer avec le scanner MoonBit serait faux sur `import { … }` et sur chaque clé nue, et écrire un second scanner pour un format qui bouge encore est un travail à courte durée de vie | |
| 68 | | Le contenu d'un bloc de `.mbt.md` | C'est un document Markdown, et c'est Markdown qui le colore. Un bloc délimité est d'une seule couleur quel que soit le langage qu'il annonce — c'est la règle de turbo-core, et elle s'applique à `mbt` exactement comme à `bash` | |
| 69 | |
| 70 | **Le seul endroit où cette réponse est visiblement fausse est une chaîne à l'intérieur d'une interpolation.** `"a \{f("x")} c"` est un seul littéral pour le compilateur et trois étendues pour le scanner — chaîne, puis `x` en identifiant, puis chaîne — parce que le premier guillemet non échappé est pris pour le fermant. C'est le prix du refus d'analyser, c'est borné (les étendues restent ordonnées et ne se chevauchent jamais, donc rien en aval ne se dérègle), et `demos/syntax-tour/tour.mbt` contient une ligne qui le montre plutôt que de l'éviter. |
| 71 | |
| 72 | **`package` est le seul débordement délibéré**, et il mérite d'être nommé comme tel. Dans un fichier `.mbt` ce n'est qu'un mot réservé ; dans les fichiers d'interface `.mbti` que cet éditeur colore aussi, c'est un vrai mot-clé. Un seul scanner sert les deux, et le colorer en mot-clé dit dans un `.mbt` exactement ce que le compilateur s'apprête à dire : ce mot ne vous appartient pas. |
| 73 | |
| 74 | ## Les huit autres langages viennent gratuitement |
| 75 | |
| 76 | TOML, YAML, Markdown, JavaScript, HTML, XML, Dockerfile et shell sont colorés par turbo-core, pas ici. Un projet MoonBit a un `moon.mod`, un `README.md`, quelques scripts, un workflow CI en YAML et souvent un Dockerfile, et un éditeur qui ne colorerait que les fichiers `.mbt` obligerait à le quitter pour tout le reste. |
| 77 | |
| 78 | Qu'ils soient partagés plutôt que copiés est tout l'intérêt de la bibliothèque : ils ont été écrits une fois, pour Turbo Go, et Turbo MoonBit les a obtenus en important un paquet. |
| 79 | |
| 80 | ## La complétion, et pourquoi elle peut échouer en silence |
| 81 | |
| 82 | Turbo MoonBit ne sait rien du système de types de MoonBit et n'essaie pas d'en savoir. Il interroge moon-lsp par le Language Server Protocol et dessine la réponse. |
| 83 | |
| 84 | Trois choses méritent d'être connues, car toutes trois ressemblent à « la complétion est cassée » : |
| 85 | |
| 86 | **moon-lsp ne répond rien tant qu'il n'a pas indexé assez du projet.** jedi résout un nom en suivant les imports vers l'extérieur, ce qui, à la première demande touchant une grosse dépendance, veut dire lire beaucoup du code de quelqu'un d'autre. Ce que l'on voit en attendant, c'est une liste vide. |
| 87 | |
| 88 | **Un serveur lancé à la mauvaise racine charge le mauvais code, puis ne répond plus rien du tout — sans erreur.** C'est pourquoi l'éditeur remonte depuis le fichier jusqu'au `moon.mod`, `moon.mod.json` ou `moon.mod.json` le plus proche plutôt que d'utiliser le répertoire courant, et c'est la façon la plus déroutante dont la complétion peut échouer. |
| 89 | |
| 90 | **Un serveur installé sans ses extras répond aux questions mais ne signale jamais un problème de lui-même.** Les linters de moon-lsp sont des dépendances optionnelles ; installé nu, il complète et saute parfaitement bien, et publie une liste *vide* de diagnostics pour un fichier qui ne s'analyse même pas. Une gouttière vide parce que le serveur n'a pas de linter et une gouttière vide parce que le code est correct sont indiscernables. C'est pourquoi [la commande d'installation nomme les extras](../how-to/enable-completion.md) et pourquoi l'installateur les vérifie. |
| 91 | |
| 92 | La réponse de l'éditeur aux deux premières est [Run ▸ Language server status](../reference/menus.md), qui dit ce qu'il a trouvé, où il l'a lancé et s'il est prêt — parce que « rien ne s'est passé » n'est pas quelque chose sur quoi un utilisateur peut agir. |
| 93 | |
| 94 | ## Neuf questions, une connexion — et les deux auxquelles moon-lsp ne répond pas |
| 95 | |
| 96 | La complétion est ce que le serveur de langage fait de plus bruyant et de moins révélateur. La même connexion pose huit questions de plus, et elles se répartissent en trois familles selon la forme de la réponse. |
| 97 | |
| 98 | **Quelque chose à lire.** `hover` — qu'est-ce que c'est ? — dessiné dans une boîte. |
| 99 | |
| 100 | **Des endroits dans le code.** `definition`, `typeDefinition`, `implementation`, `references`. Une requête chacun, une seule forme de réponse pour les quatre, ce qui explique qu'ils soient une seule fonction en dessous. Un seul endroit est ouvert ; plusieurs sont proposés en liste, parce qu'une réponse unique est l'exception plutôt que la règle — une méthode utilisée dans tout un paquet a autant de références que quelqu'un a pris la peine d'en écrire, et pendant longtemps cet éditeur prenait la première et jetait le reste. |
| 101 | |
| 102 | **Des noms.** `documentSymbol` pour le plan d'un fichier, `workspace/symbol` pour une recherche à travers le projet. Le protocole a trois formes pour un symbole et l'éditeur en veut une, si bien que l'aplatissement se fait là où les réponses arrivent plutôt que là où elles sont dessinées. |
| 103 | |
| 104 | Et une chose que personne ne demande : **`publishDiagnostics` arrive sans y être invité**, dès que le serveur a un avis, pour tous les fichiers qu'il a chargés — qui sont d'ordinaire plus nombreux que celui qu'on a sous les yeux. C'est pourquoi Problems liste tous les fichiers plutôt que le fichier courant, et pourquoi la marque dans la gouttière apparaît sans qu'on ait appuyé sur quoi que ce soit. |
| 105 | |
| 106 | **Deux des neuf reviennent vides avec moon-lsp, et c'est la limite du serveur plutôt que celle de l'éditeur.** moon-lsp n'annonce ni `typeDefinition` ni `implementation` : **Code ▸ Type definition** et **Code ▸ Find implementations** ne signalent donc rien. Tout le reste fonctionne, y compris la recherche de symboles à l'échelle du projet, à laquelle le serveur de Turbo Python ne répond pas. C'est écrit plutôt que caché parce que l'alternative — griser deux entrées de menu selon ce qu'un serveur a dit au démarrage — donne au menu une forme différente selon les machines, et un utilisateur qui a lu cette page en sait plus qu'un utilisateur tombé sur une entrée grisée. |
| 107 | |
| 108 | L'éditeur ne demande rien de tout cela avant que le serveur se soit dit prêt, et il dit de laquelle il s'agit quand une question ne peut pas trouver de réponse. « Rien trouvé » et « je n'ai pas fini de charger » sont la même réponse vide et deux nouvelles très différentes ; les confondre est la façon la plus déroutante dont la complétion ait jamais échoué ici. |
| 109 | |
| 110 | ## Rapport avec le reste |
| 111 | |
| 112 | - Ce qui est reconnu exactement : [Langages colorés](../reference/languages.md) |
| 113 | - Faire marcher la complétion : [Comment activer la complétion MoonBit](../how-to/enable-completion.md) |
| 114 | - Où vit le scanner et pourquoi : [Architecture](architecture.md) |