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

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

colouring-and-completion.md · 110 lines · 17.4 KBmarkdown Blame HistoryRaw
📦 Turbo JS 91999d1 k33g 12h ago1# Coloration et complétion — explication
2
3## De quoi s'agit-il ?
4
5Les deux fonctionnalités qui font de Turbo JS un éditeur *pour JavaScript* plutôt qu'un éditeur de texte qui ouvre des fichiers `.js` : 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
9La coloration est faite ici, dans quelque six cents lignes de Go écrites à la main. La complétion est faite par `typescript-language-server` — le moteur même qui est derrière le support JavaScript de VS Code — et Turbo JS ne fait que demander et dessiner.
10
11Cette séparation n'est pas un accident d'effort. La coloration doit être **instantanée et tolérante** : elle s'exécute à chaque frappe, sur un texte invalide la plupart du temps pendant qu'on le tape, et un colorateur qui s'arrête pour réfléchir ou renonce devant une entrée cassée est pire que pas de colorateur. La complétion doit être **juste**, ce qui pour JavaScript signifie parser le fichier, suivre ses lignes `import` et `require` dans `node_modules`, inférer des types à travers un langage qui n'en déclare aucun, et savoir ce que prend chaque méthode de chaque globale — et rien de ce qui doit être instantané ne peut aussi être cela.
12
13L'éditeur dessine donc des couleurs qu'il a calculées lui-même, et affiche des complétions que quelqu'un d'autre a calculées.
14
15## Pourquoi JavaScript est scanné ici alors que la bibliothèque le scanne déjà
16
17turbo-core colore JavaScript pour tous les éditeurs de la famille, parce que tous le rencontrent — dans le bloc de code d'un README, dans le `<script>` d'une page web, dans le front-end d'un projet écrit dans autre chose. Son scanner est écrit pour cet usage : les mots-clés, les chaînes, les template literals, les commentaires, les nombres, les globales courantes, et un refus délibéré de reconnaître les expressions régulières.
18
19Le refus est raisonné. Distinguer `/x/g` de `a / b / c` exige de savoir si le token avant la barre oblique peut terminer une expression, et une mauvaise supposition colore le reste d'une ligne comme une chaîne — un échec pire que de laisser une expression régulière de la couleur d'un opérateur, pour un langage qu'on ne fait que traverser.
20
21Un éditeur *pour* JavaScript fait le marché inverse, et peut se le permettre. Les expressions régulières sont une ligne sur deux dans un programme Node — routage, parsing, validation — et un lecteur veut les distinguer des chaînes. Et la supposition peut être **bornée** : un littéral d'expression régulière ne peut pas contenir de retour à la ligne, une barre oblique sans barre fermante sur sa ligne ne peut donc pas en ouvrir un, quoi qu'il y ait eu avant. La règle ici est celle que tous les éditeurs suivent — une barre oblique après une valeur divise, une barre oblique après un opérateur, un crochet, un mot-clé ou en début de ligne ouvre un littéral — avec cette borne en plus, si bien qu'une mauvaise supposition coûte au plus une ligne et jamais le reste du fichier.
22
23L'éditeur enregistre donc un scanner à lui sous le nom de la bibliothèque, `javascript`, et celui de la bibliothèque est remplacé. C'est une porte que la bibliothèque a construite à dessein : `syntax.Register` dit que l'enregistrement le plus tardif l'emporte parce qu'il est l'énoncé le plus spécifique. Rien dans turbo-core n'a changé, et un fichier de snippets disant `languages = ["javascript"]` ou un bloc ```js dans une fenêtre d'agent atteint ce scanner sans savoir lequel c'est.
24
25Ce que le remplacement ajoute, outre les expressions régulières : la ligne shebang, les globales de Node, le nom après `function` et `class`, les `#names` privés, les `@decorators`, `...` et `?.` comme portées uniques, les identifiants Unicode, et une majuscule initiale lue comme une classe.
26
27## Ce qui franchit un retour à la ligne, et pourquoi si peu le fait
28
29Deux constructions peuvent courir d'une ligne à la suivante, et chacune est portée comme un drapeau plutôt qu'une profondeur parce qu'aucune ne s'imbrique :
30
31- **Un commentaire bloc** court de `/*` au premier `*/`, où qu'il soit. JavaScript ne les imbrique pas — `/* a /* b */ c */` se termine après `b`, et `c */` est du code — une profondeur serait donc une affirmation sur un autre langage.
32- **Un template literal** court d'un accent grave au prochain accent grave non échappé, interpolations comprises. C'est la seule chaîne du langage qui peut contenir un vrai retour à la ligne.
33
34Une chaîne ordinaire ne franchit pas une ligne. `'…'` et `"…"` se terminent à leur guillemet ou à la fin de leur ligne, parce que le langage dit qu'un retour à la ligne à l'intérieur est une erreur, si bien qu'une chaîne non terminée colore le reste de sa ligne et rien de plus — et la ligne suivante est de nouveau du code. Une expression régulière fait de même. C'est l'inverse de ce que le scanner de Turbo Golo fait avec les chaînes de Golo, et pour la raison inverse : le lexer de Golo lit jusqu'au guillemet fermant où qu'il soit, celui de JavaScript s'y refuse.
35
36## Où le scanner s'appuie sur le langage, et où sur la convention
37
38**Les mots-clés, les constantes et les globales sont des tables.** Les mots-clés sont les mots réservés du langage plus les mots contextuels qu'un lecteur rencontre comme mots-clés — `async`, `await`, `of`, `get`, `set`, `static`, `from`, `as`. Les globales sont les constructeurs et les espaces de noms de la bibliothèque standard, et — parce que c'est un éditeur Node — celles de Node : `process`, `Buffer`, `require`, `module`, `exports`, `__dirname`, `__filename`, `setImmediate`, et les morceaux de la plateforme web que Node livre, `fetch`, `URL`, `TextEncoder`, `AbortController`, `performance`, `crypto`. Elles sont reconnues par leur nom, comme le sont les identifiants prédéclarés de Go : un fichier qui masque `Map` le voit toujours coloré comme une globale, ce que font tous les éditeurs.
39
40**Un mot après un point est une propriété, quelle que soit son orthographe.** `map.get(k)` est un appel, `options.default` est un champ, `promise.catch(…)` est une méthode, et aucun d'eux n'est le mot-clé qu'il serait seul. Le scanner se souvient du point — et de `?.` — et lit le mot suivant comme un nom. C'est la règle qui sépare un scanner JavaScript d'une liste de mots-clés : une Map a une méthode appelée `get` et `set`, une Promise une appelée `catch` et `finally`, un objet une propriété appelée `default`, et les colorer comme des mots-clés ferait paraître réservée chaque ligne de code ordinaire.
41
42**Un mot-clé contextuel qu'on appelle est une fonction.** `get name() {}` dans le corps d'une classe a `get` en mot-clé ; `get(key)` sans rien devant est une fonction appelée `get`. Le mot seul ne peut pas le dire ; la parenthèse qui le suit le peut. `async` est laissé hors de cette règle à dessein — `async (x) => x` est une fonction fléchée, et `async` n'y est pas un appel.
43
44**Un nom capitalisé est une classe, et c'est ici une convention plutôt qu'une règle.** JavaScript n'a pas de règle de casse : `const Count = 1` est légal. Mais les classes et les constructeurs sont capitalisés par tout le monde — `Greeter`, `EventEmitter`, `MyError` — et rien d'autre ne l'est d'ordinaire, le scanner colore donc selon la convention, comme Turbo Rust et Turbo Golo le font pour leurs langages. Cela tient même devant une parenthèse, parce que `new Greeter()` est un constructeur et non un appel. Ce que cela coûte : une constante en `SCREAMING_SNAKE_CASE` est colorée comme une classe aussi — rien dans l'orthographe ne sépare les deux conventions, et la référence le dit plutôt que de laisser quelqu'un le découvrir.
45
46**Le nom après `function` est une fonction, et le nom après `class` est une classe, par position.** Partout ailleurs un nom est une fonction parce qu'une parenthèse le suit, et une déclaration est un endroit où c'est déjà vrai — `function parse(input)` a sa parenthèse — mais le `*` d'un générateur et le nom d'une classe ne l'ont pas, tous deux sont donc lus d'après ce qui les précède plutôt que ce qui les suit.
47
48**Une expression régulière est colorée comme un littéral de caractère.** L'ensemble des classes est clos — dix-sept classes, pour qu'un seul thème colore tous les langages qu'un éditeur apprendra jamais — et JavaScript n'a pas de littéral de caractère, `syntax.char` est donc libre. Une expression régulière est l'autre sorte de littéral délimité du langage, et vaut d'être distinguée d'une chaîne par la couleur ; dans `turbo-classic` les deux partagent par hasard un vert, et d'autres thèmes les séparent.
49
50**Les noms peuvent être presque n'importe quoi.** Les identifiants JavaScript sont Unicode : `café` et `名前` sont des noms, et `$` et `_` aussi, que jQuery et lodash ont rendus ordinaires. Le scanner utilise le `unicode.IsLetter` de Go plutôt que le prédicat ASCII de turbo-core, et ils sont donc colorés.
51
52## Ce que le scanner refuse de deviner
53
54Là où une construction ne peut pas être reconnue à partir de ce qu'une ligne contient, elle est laissée telle quelle plutôt qu'approximée. Un colorateur qui se trompe est pire qu'un colorateur qui se tait :
55
56| Non reconnu | Parce que |
57| --- | --- |
58| Le code à l'intérieur de `${…}` | Le colorer signifie que le scanner se rappelle lui-même avec une profondeur d'imbrication à porter — une interpolation peut contenir un template qui contient une interpolation — pour une construction qui n'est d'ordinaire qu'une courte expression. Tout le littéral est une chaîne, et un accent grave dans une interpolation le termine prématurément |
59| JSX | `<div className="x">` est du balisage dans une expression, et un scanner JSX est un scanner HTML qui rend la main à un scanner JavaScript à chaque accolade. C'est un autre langage, avec sa propre extension, et ce n'est pas celui-ci |
60| TypeScript | Le serveur de langage sert les fichiers `.ts` sans qu'on le lui demande, et le scanner ne les colore pas. Les mots-clés de TypeScript, ses annotations de type et ses génériques sont un scanner à eux, et un éditeur qui en colorerait la moitié se tromperait exactement dans les lignes qui font d'un fichier du TypeScript plutôt que du JavaScript |
61| Si `MAX_SIZE` est une constante | La majuscule initiale dit classe, et il n'y a pas de seconde règle qui ne colorerait pas de travers une classe dont le nom est un acronyme |
62| Si un nom est lié dans cette portée | Rien ici ne lit plus d'une ligne à la fois ; c'est la question du serveur de langage, et [F1 y répond](../how-to/ask-about-code.md) |
63
64## JSON, et pourquoi il est ici
65
66`package.json` est le premier fichier de tout projet Node et celui dont cet éditeur se sert pour trouver la racine du projet, et turbo-core ne colore pas JSON. Trente lignes dans `json.go` le font : une chaîne suivie d'un deux-points est une **clé** et est colorée comme un attribut, toute autre chaîne est une **valeur**, et les deux côtés du deux-points se lisent séparément — ce qui est tout ce qu'on attend d'un manifeste coloré. Les commentaires sont tolérés, parce que `tsconfig.json` et le fichier de réglages de chaque éditeur en ont, et un scanner qui les peindrait comme cassés se tromperait exactement dans les fichiers les plus susceptibles d'en contenir.
67
68Il est enregistré sous son propre nom, `json`, si bien qu'un bloc ```json dans une fenêtre d'agent est coloré aussi.
69
70## Les sept autres langages viennent gratuitement
71
72TOML, YAML, Markdown, HTML, XML, les Dockerfiles et le shell sont colorés par turbo-core, pas ici. Un projet Node a un `README.md`, un `compose.yaml` pour la base de données à laquelle il parle, un `Dockerfile` pour être livré, un `.github/workflows/ci.yml`, et un éditeur qui ne colorerait que les fichiers `.js` vous ferait le quitter pour le reste.
73
74Qu'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 JS les a obtenus en important un paquet. JavaScript est le huitième, et cet éditeur l'a repris plutôt que d'en hériter.
75
76## La complétion, et pourquoi elle peut échouer en silence
77
78Turbo JS ne sait rien de la sémantique de JavaScript et n'essaie pas. Il interroge `typescript-language-server` par le Language Server Protocol et dessine la réponse.
79
80Quatre choses valent d'être sues à ce propos, parce que toutes quatre ressemblent à « la complétion est cassée » :
81
82**Le serveur a besoin de TypeScript 6 à côté de lui, pas de 7.** `typescript-language-server` est une fine couche au-dessus du `tsserver.js` du paquet `typescript`, et il ne dépend pas de ce paquet : vous installez les deux. TypeScript 7 est le portage natif, et il livre un compilateur avec un serveur de langage à lui à l'intérieur et aucun `tsserver.js` — si bien qu'un serveur installé à côté d'un `typescript` courant démarre puis refuse, avec *Could not find a valid TypeScript installation*, avant que l'éditeur lui ait demandé quoi que ce soit. L'indication d'installation épingle `typescript@6` exactement pour cette raison, et la barre d'état montre cette indication quand le serveur manque.
83
84**Pourquoi pas le serveur de TypeScript 7 lui-même, alors.** `tsc --lsp --stdio` a été mesuré contre les mêmes neuf tests de bout en bout que ce dépôt lance contre `typescript-language-server`. Il répond aux huit requêtes, quatre fois plus vite, et il ne publie **aucun** diagnostic — il les propose en mode *pull*, par `textDocument/diagnostic`, une requête que turbo-core ne fait pas. Un éditeur dont la gouttière est vide parce que le serveur attend qu'on lui demande ressemble exactement à un éditeur qui n'a rien à signaler, la paire mûre est donc celle qui est nommée ici. Le jour où la bibliothèque saura tirer les diagnostics, un seul `npm install -g typescript` sera la meilleure réponse.
85
86**Les diagnostics du JavaScript pur sont des erreurs de syntaxe.** `tsserver` ne vérifie les types d'un fichier `.js` que si on le lui demande — avec `// @ts-check` en tête du fichier, ou `checkJs` dans un `jsconfig.json` — si bien qu'un fichier `.js` avec une erreur de type ne reçoit aucune marque, et un fichier avec un crochet manquant en reçoit une. C'est la conception du serveur, et elle est écrite ici plutôt que contournée.
87
88**Le projet est là où est `package.json`.** Le serveur est démarré dans le répertoire le plus proche, au niveau du fichier ouvert ou au-dessus, qui contient un `package.json`, parce que c'est là qu'est `node_modules` et de là que les lignes `import` se résolvent. Ouvrez un fichier d'un autre projet dans la même session et le serveur en parle du point de vue du premier projet.
89
90La réponse de l'éditeur à la première est [Run ▸ Language server status](../reference/menus.md), qui dit ce qu'il a trouvé, où il l'a démarré et s'il est prêt — parce que « rien ne s'est passé » n'est pas quelque chose qu'un utilisateur peut traiter.
91
92## Neuf questions, une connexion
93
94La complétion est la chose la plus bruyante que fait le serveur de langage et la moins révélatrice. La même connexion pose huit autres questions, et elles se répartissent en trois sortes selon ce qui revient.
95
96**Quelque chose à lire.** `hover` — qu'est-ce que c'est ? — dessiné dans une boîte. Pour une fonction que vous avez déclarée, c'est sa signature et le commentaire JSDoc écrit juste au-dessus ; pour une méthode d'une globale, sa signature et la documentation qu'en donne la bibliothèque standard.
97
98**Des endroits dans le code.** `definition`, `typeDefinition`, `implementation`, `references`. Une requête chacune, une seule forme de réponse à elles quatre, c'est pourquoi elles sont 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 — les références à une fonction déclarée une fois et appelée deux fois sont trois endroits, et les implémentations d'une classe avec deux sous-classes en sont deux.
99
100**Des noms.** `documentSymbol` pour le plan d'un fichier — ses fonctions et ses classes, les méthodes imbriquées sous leur classe — et `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 plutôt que là où elles sont dessinées.
101
102Et une chose que personne ne demande : **`publishDiagnostics` arrive sans y être invité**, chaque fois que le serveur a un avis, à l'ouverture et à chaque modification. C'est pourquoi la marque dans la gouttière apparaît sans qu'on ait appuyé sur rien.
103
104`typescript-language-server` répond aux neuf pour le JavaScript pur, et un test de ce dépôt pilote chacune d'elles contre le vrai serveur, ce paragraphe ne peut donc pas vieillir en silence. L'éditeur ne demande rien de tout cela avant que le serveur se dise prêt, et dit de quel cas il s'agit quand une question ne peut pas recevoir de réponse. « Rien trouvé » et « je n'ai pas fini de charger » sont la même réponse vide et des nouvelles très différentes ; les confondre est la façon la plus déroutante dont la complétion ait jamais échoué ici.
105
106## Liens avec le reste
107
108- Exactement ce qui est reconnu : [Langages colorés](../reference/languages.md)
109- Faire fonctionner la complétion : [Activer la complétion](../how-to/enable-completion.md)
110- Où vit le scanner et pourquoi : [Architecture](architecture.md)