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.

📦 Turbo JS 91999d1 · on v1.0.2 · k33g · 10h ago
colouring-and-completion.md · 110 lines · 17.4 KBmarkdown
Blame HistoryOpen raw

Coloration et complétion — explication

De quoi s'agit-il ?

Les 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.

La coloration est à nous ; la complétion ne l'est pas

La 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.

Cette 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.

L'é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.

Pourquoi JavaScript est scanné ici alors que la bibliothèque le scanne déjà

turbo-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.

Le 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.

Un é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.

L'é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.

Ce 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.

Ce qui franchit un retour à la ligne, et pourquoi si peu le fait

Deux 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 :

  • 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.
  • 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.

Une 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.

Où le scanner s'appuie sur le langage, et où sur la convention

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.

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.

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.

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.

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.

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.

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.

Ce que le scanner refuse de deviner

Là 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 :

Non reconnu Parce que
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
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
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
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
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

JSON, et pourquoi il est ici

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.

Il est enregistré sous son propre nom, json, si bien qu'un bloc ```json dans une fenêtre d'agent est coloré aussi.

Les sept autres langages viennent gratuitement

TOML, 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.

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 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.

La complétion, et pourquoi elle peut échouer en silence

Turbo 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.

Quatre choses valent d'être sues à ce propos, parce que toutes quatre ressemblent à « la complétion est cassée » :

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.

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.

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.

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.

La réponse de l'éditeur à la première 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 « rien ne s'est passé » n'est pas quelque chose qu'un utilisateur peut traiter.

Neuf questions, une connexion

La 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.

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.

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.

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.

Et 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.

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.

Liens avec le reste

  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
# Coloration et complétion — explication

## De quoi s'agit-il ?

Les 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.

## La coloration est à nous ; la complétion ne l'est pas

La 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.

Cette 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.

L'é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.

## Pourquoi JavaScript est scanné ici alors que la bibliothèque le scanne déjà

turbo-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.

Le 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.

Un é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.

L'é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.

Ce 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.

## Ce qui franchit un retour à la ligne, et pourquoi si peu le fait

Deux 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 :

- **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.
- **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.

Une 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.

## Où le scanner s'appuie sur le langage, et où sur la convention

**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.

**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.

**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.

**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.

**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.

**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.

**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.

## Ce que le scanner refuse de deviner

Là 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 :

| Non reconnu | Parce que |
| --- | --- |
| 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 |
| 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 |
| 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 |
| 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 |
| 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) |

## JSON, et pourquoi il est ici

`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.

Il est enregistré sous son propre nom, `json`, si bien qu'un bloc ```json dans une fenêtre d'agent est coloré aussi.

## Les sept autres langages viennent gratuitement

TOML, 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.

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 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.

## La complétion, et pourquoi elle peut échouer en silence

Turbo 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.

Quatre choses valent d'être sues à ce propos, parce que toutes quatre ressemblent à « la complétion est cassée » :

**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.

**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.

**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.

**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.

La 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.

## Neuf questions, une connexion

La 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.

**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.

**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.

**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.

Et 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.

`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.

## Liens avec le reste

- Exactement ce qui est reconnu : [Langages colorés](../reference/languages.md)
- Faire fonctionner la complétion : [Activer la complétion](../how-to/enable-completion.md)
- Où vit le scanner et pourquoi : [Architecture](architecture.md)