Coloration et complétion — explication
De quoi s'agit-il ?
Les deux fonctionnalités qui font de Turbo Rust un éditeur pour Rust plutôt qu'un éditeur de texte qui se trouve ouvrir des fichiers .rs : 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.
La coloration est à nous ; la complétion ne l'est pas
La coloration se fait ici, en six cents lignes de Go écrites à la main. La complétion se fait dans rust-analyzer, et Turbo Rust se contente de demander et de dessiner.
Ce partage n'est pas un accident d'effort. La coloration doit être instantanée et tolérante : elle tourne à chaque frappe, sur un texte invalide la plupart du temps pendant qu'on le tape, et un coloriseur qui s'arrête pour réfléchir ou qui abandonne devant une entrée cassée est pire que pas de coloriseur du tout. La complétion doit être correcte, ce qui pour Rust veut dire connaître le système de traits, le graphe des caisses et l'API publique de chaque dépendance — et rien qui doive être instantané ne peut aussi être cela.
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.
Pourquoi Rust est analysé à la main
Go a un analyseur lexical dans sa bibliothèque standard, et Turbo Go s'en sert : go/scanner est le code qu'utilise le compilateur, si bien que l'éditeur et le compilateur s'accordent sur ce qu'est un jeton, sans rien à maintenir en phase.
Rust n'a rien de tel disponible ici. rustc n'est pas une bibliothèque Go, et l'analyseur de rust-analyzer est une caisse Rust. Le choix était entre un analyseur écrit à la main et un appel à un programme externe à chaque frappe.
Ce sera l'analyseur. Six cents lignes, un fichier chacun pour l'aiguillage, les littéraux et les mots — et aucune tentative de moteur général. Pas de langage de motifs, pas de format de grammaire, pas de table d'expressions régulières : c'est du Go ordinaire qu'un lecteur peut suivre, ce qui est la règle que suivent aussi les huit analyseurs de turbo-core.
Les trois choses qui traversent un saut de ligne
Presque tout en Rust se décide à partir de la ligne qu'on a sous les yeux. Trois choses non, et chacune est transportée explicitement plutôt qu'approximée :
Les commentaires de bloc, avec leur profondeur. Rust les imbrique : /* a /* b */ c */ est un seul commentaire. Un simple drapeau « dans un commentaire » le ferme au premier */ et colore c */ comme du code — ce n'est pas un échec subtil, c'est un demi-écran de la mauvaise couleur. L'état est donc un entier.
Les chaînes brutes, avec leur nombre de dièses. r#"a "quoted" thing"# se termine à un guillemet suivi d'exactement le nombre de dièses par lequel elle s'est ouverte, et n'a aucun échappement. Transporter un booléen la fermerait au guillemet intérieur.
Les chaînes ordinaires. Rust autorise un vrai saut de ligne à l'intérieur de "…", si bien qu'une chaîne qui dépasse la fin d'une ligne n'est pas l'état d'erreur qu'elle serait dans la plupart des langages.
Tout le reste — les attributs compris — se décide à l'intérieur d'une ligne. Un attribut qui ne se ferme pas est coloré jusqu'au bout de sa ligne et n'est pas transporté, parce qu'un #[ non fermé est presque toujours un attribut à moitié tapé, et le transporter repeindrait le reste du fichier.
La seule véritable ambiguïté
' ouvre un littéral de caractère et une durée de vie, et Rust tranche par ce qui suit : 'a' est un caractère, 'a une durée de vie.
La règle ici consiste à chercher le guillemet fermant là où un littéral de caractère devrait le mettre — une rune plus loin, ou davantage pour un échappement — et à lire une durée de vie quand il n'y est pas. Cela donne le bon résultat pour 'static, '\n', 'a', '\u{1F600}' et 'a, à partir de la seule ligne.
Se tromper coûte cher : lire 'a comme un caractère non terminé transforme le reste de la ligne en chaîne. fn longest<'a>(x: &'a str) -> &'a str en contient trois, et ce cas a son propre test.
Là où l'analyseur s'appuie sur la convention
Une majuscule initiale veut dire un type. La convention de nommage de Rust est assez forte pour qu'on s'en serve : un type, un trait et une variante d'énumération sont tous en UpperCamelCase, et rien d'autre ne l'est. C'est une heuristique, pas une règle, et cela se voit à un seul endroit — une constante en SCREAMING_SNAKE_CASE est colorée comme un type.
On pourrait corriger cela par une seconde règle (« tout en majuscules et soulignés veut dire une constante »), et cela n'a pas été fait : la règle mal-colorerait alors un type dont le nom est un acronyme, et échanger une mauvaise réponse contre une autre n'est pas un progrès. La référence le dit franchement plutôt que de laisser quelqu'un le découvrir.
Ce que l'analyseur refuse de deviner
Là où une construction ne peut pas être reconnue à partir d'une seule ligne, elle est laissée telle quelle plutôt qu'approximée. Un coloriseur qui se trompe est pire qu'un coloriseur silencieux :
| Non reconnu | Parce que |
|---|---|
| Quelle macro est invoquée | println! et une macro que vous avez écrite sont toutes deux des builtins ; les distinguer demande l'expansion de la caisse |
L'intérieur d'un corps de macro_rules! |
Coloré comme du Rust ordinaire, ce qui est le plus souvent juste et parfois non |
Le Markdown à l'intérieur d'un commentaire /// |
Un commentaire de documentation est un commentaire ; colorer deux langages à la fois est le moteur général qu'on n'a pas ici |
Les huit autres langages viennent gratuitement
TOML, YAML, Markdown, JavaScript, HTML, XML, les Dockerfiles et le shell sont colorés par turbo-core, pas ici. Un projet Rust a un Cargo.toml, un README.md, quelques scripts, un workflow d'intégration continue en YAML et souvent un Dockerfile, et un éditeur qui ne colorerait que les fichiers .rs vous ferait le quitter pour tout le reste.
Qu'ils soient partagés plutôt que copiés, c'est tout l'intérêt de la bibliothèque : ils ont été écrits une fois, pour Turbo Go, et Turbo Rust les a obtenus en important un paquet.
La complétion, et pourquoi elle peut échouer en silence
Turbo Rust ne connaît rien au système de types de Rust et n'essaie pas. Il interroge rust-analyzer par le Language Server Protocol et dessine la réponse.
Deux choses méritent d'être sues, parce que les deux ressemblent à « la complétion est cassée » :
rust-analyzer ne répond rien tant qu'il n'a pas chargé l'espace de travail. Il lit Cargo.toml, résout le graphe de dépendances et l'indexe, ce qui prend quelques secondes sur une petite caisse et bien plus sur une grosse. Il le signale par une notification $/progress que ce client ne lit pas, si bien que ce que l'on voit en attendant est une liste vide.
Un serveur lancé à la mauvaise racine charge le mauvais code, puis ne répond rien du tout — sans erreur. C'est pourquoi l'éditeur remonte du fichier jusqu'au Cargo.toml 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.
La réponse de l'éditeur aux deux est Run ▸ Language server status, qui dit ce qu'il a trouvé, où il l'a démarré et s'il est prêt — parce que « il ne s'est rien passé » n'est pas quelque chose sur quoi un utilisateur peut agir.
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 — un trait a autant d'implémentations que quelqu'un a pris la peine d'en écrire, 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.
Comment cela se relie au reste
- Exactement ce qui est reconnu : Langages colorés
- Faire marcher la complétion : Comment activer la complétion Rust
- Où vit l'analyseur et pourquoi : Architecture
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 |
|