Serveurs de langage
Textchum valide le code via le Language Server Protocol, avec un comportement fondateur : une instance de serveur par projet.
Une instance par projet
Les processus serveurs sont identifiés par (serveur, racine du projet),
avec la même notion de projet que le navigateur : le
répertoire ancêtre le plus proche portant un marqueur de racine. Ouvrez
des fichiers de deux projets Rust différents et deux processus
rust-analyzer indépendants tournent, chacun initialisé avec sa propre
racine, chacun ne voyant que les fichiers de son projet. Les fuites entre
projets — des diagnostics d'un espace de travail débordant dans un autre,
un index bâti sur tout le répertoire personnel — sont impossibles par
construction.
Les fichiers hors de tout projet reçoivent une instance par répertoire ; les fichiers isolés ne rejoignent donc jamais l'espace de travail de quelqu'un d'autre.
Ce que l'on voit
- Les résultats arrivent pendant la frappe (envoyés par lots avec temporisation) et marquent le texte concerné : rouge pour les erreurs, orange pour les avertissements, bleu pour les notes.
- Le sous-titre de la fenêtre les compte (« 2 errors, 1 warning »).
- La complétion pendant la frappe : les suggestions apparaissent
après les caractères d'identifiant et
., filtrées au fil de la frappe — ↑/↓ pour choisir, ⏎ ou ⇥ pour accepter, ⎋ pour fermer, ⌃Espace pour les demander explicitement. - Laisser la souris sur un symbole affiche la documentation hover du serveur dans une bulle, avec le Markdown envoyé par les serveurs déjà rendu — blocs de code en chasse fixe, emphase et code en ligne stylés. Elle ne se déclenche que sur les identifiants (jamais sur les espaces ni les commentaires), se désactive dans Présentation ▸ Documentation au survol (ou dans les Réglages), et Afficher la documentation du symbole (⌃⌘H) la demande pour le symbole sous le curseur — même souris désactivée.
- Aller à la définition (⌃⌘J, ou ⌘-clic) rejoint le symbole sous le curseur — d'un fichier à l'autre, en ouvrant ou en ramenant la cible au premier plan au besoin. Sur la définition il n'a nulle part où aller, alors il répond à la question qui reste : qui s'en sert. Un usage est un saut, plusieurs ouvrent la liste, et un symbole auquel rien ne renvoie le dit. Un serveur qui répond par plusieurs définitions — une déclaration et son implémentation — les propose de la même façon. Le raccourci de recherche des références ne change pas.
- Chercher les références (⇧⌘R) liste chaque usage du symbole sous
le curseur dans un panneau flottant — ↑/↓ pour se déplacer, ⏎ pour
sauter. Le code d'abord, les tests ensuite, chacun sous un titre
avec son compte : ce qui appelle ceci est la question, ce qui le
vérifie est la suite. Quels fichiers sont des tests est une
convention et non un fait — un répertoire
tests, unparser_test.go, unButton.test.ts, unParserTests.swift— donc la règle est prudente, etlatest.rsn'est pas un test. Un#[cfg(test)] mod testsde Rust dans un fichier ordinaire est listé comme du code, ce que dit son chemin. Si tout tombe du même côté, il n'y a pas de titres. - Formater le document (⌥⇧⌘F) demande d'abord au serveur puis retombe sur la chaîne de préprocesseurs de sauvegarde — le formatage marche donc sur les documents sans titre et les langages sans serveur, dès qu'une chaîne est configurée.
- Une ligne marquée se lit. Poser le pointeur sur un soulignement montre ce qu'a dit le serveur, et Afficher le diagnostic de la ligne (⌃⌘E, Ctrl+Alt+E sous Linux) dit la même chose pour la ligne du curseur — le curseur est d'ordinaire en fin de ligne plutôt que dans la marque, c'est donc la ligne qui répond. Le message nomme sa gravité : un soulignement dit seulement que quelque chose ne va pas, et un avertissement ne doit pas se lire comme une erreur. Sans aller-retour : le diagnostic est déjà là.
- Diagnostics… (⇧⌘E, Ctrl+Shift+E sous Linux) liste tous les diagnostics du document dans l'ordre où ils apparaissent — celui dans lequel on les corrige et celui de la gouttière — avec la gravité sur chaque ligne. ⏎ y saute, et le saut entre dans la pile de retour.
- Actions de code… (⌘., Ctrl+. sous Linux) demande ce que le
serveur peut faire de l'endroit où est le curseur — importer ce nom,
ajouter la branche manquante, ôter la variable inutilisée — et liste
ce qui revient, la suggestion du serveur étant signalée comme telle.
Les signalements sous le curseur accompagnent la requête tels que le
serveur les a publiés,
codeetdatacompris : c'est ainsi qu'un serveur reconnaît ce qu'il a lui-même trouvé, et un signalement reconstruit ne lui dit rien. Une action que le serveur a envoyée sans sa modification lui est renvoyée pour qu'il la termine avant qu'elle soit appliquée, et une action qui porte une commande plutôt qu'une modification est exécutée par le serveur. - Renommer le symbole… (⌃⌘R) renomme dans tout l'espace de travail : les fenêtres ouvertes sont éditées sur place (l'annulation fonctionne par fenêtre) et les fichiers que personne n'a ouverts sont réécrits sur disque.
- Formater le document (⌥⇧⌘F) reformate via le serveur, en gardant les tabulations si le document indente avec des tabulations, des espaces sinon.
- Plan du document (⇧⌘O) liste les symboles du fichier — l'imbrication rendue par l'indentation, filtrable en flou — et ⏎ saute vers la sélection.
- Un serveur manquant est signalé une seule fois, avec la commande qui l'installe ; tout le reste de l'éditeur continue de fonctionner sans lui.
Serveurs
Textchum trouve les serveurs sur le PATH — il ne les installe pas :
| Langage | Serveur | Installation |
|---|---|---|
| Rust | rust-analyzer | rustup component add rust-analyzer |
| Python | pyright | npm install -g pyright |
| Go | gopls | go install golang.org/x/tools/gopls@latest |
| C | clangd | Xcode CLT, ou brew install llvm |
| JavaScript | typescript-language-server | npm install -g typescript-language-server typescript |
| Swift | sourcekit-lsp | fourni avec la chaîne d'outils Xcode |
| Zig | zls | brew install zls |
| Bash | bash-language-server | npm install -g bash-language-server |
| C++ | clangd | Xcode CLT, or brew install llvm |
| TypeScript | typescript-language-server | npm install -g typescript-language-server typescript |
| Java | jdtls | brew install jdtls |
| C# | csharp-ls | dotnet tool install --global csharp-ls |
| Ruby | ruby-lsp | gem install ruby-lsp |
| Lua | lua-language-server | brew install lua-language-server |
| Haskell | haskell-language-server | ghcup install hls |
| OCaml | ocamllsp | opam install ocaml-lsp-server |
| Scala | metals | cs install metals |
| Nix | nil | nix profile install nixpkgs#nil |
| CMake | cmake-language-server | uv tool install cmake-language-server |
| JSON | vscode-json-language-server | npm install -g vscode-langservers-extracted |
| HTML | vscode-html-language-server | npm install -g vscode-langservers-extracted |
| CSS | vscode-css-language-server | npm install -g vscode-langservers-extracted |
| YAML | yaml-language-server | npm install -g yaml-language-server |
| TOML | taplo | brew install taplo |
| Markdown | marksman | brew install marksman |
Les modèles Go sont servis par gopls également, le C++ par clangd,
et TypeScript et TSX par les serveurs JavaScript. Plusieurs langages ont
plus d'un serveur enregistré : Python dispose de pyright,
basedpyright, pylsp, ruff, jedi, ty et pyrefly ;
JavaScript, TypeScript et TSX de typescript-language-server, vtsls,
deno et biome ; Ruby de ruby-lsp et solargraph.
PHP a une grammaire mais aucun serveur enregistré : ceux qui servent ne
documentent pas la ligne de commande qu'ils acceptent, et une entrée
devinée vaut moins que pas d'entrée — lsp.servers accepte celle que
vous connaissez. Le tableau
nomme celui qui sert quand la configuration ne dit rien ; les autres se
demandent par identifiant.
Choisir ses serveurs
Settings → Language Servers permet de décider quelle commande sert un
langage — pour tous les projets (un défaut) ou pour une racine de
projet précise. Les entrées de projet l'emportent sur les défauts ; les
langages sans entrée utilisent le tableau ci-dessus. Les entrées vivent
dans config.json sous "lsp", avec les garanties d'édition à la main
habituelles du fichier :
{
"lsp": {
"defaults": {"python": "pylsp"},
"projects": {"/work/projA": {"python": "pyright-langserver --stdio"}}
}
}
Le champ de langage propose les langages que cette compilation connaît et accepte toujours n'importe quel texte : un langage peut être configuré avant qu'une grammaire existe pour lui, et l'entrée sert encore quand elle arrive.
Définir un serveur que l'éditeur ne connaît pas
lsp.servers contient des entrées de la même forme que la table
intégrée : un serveur peut donc être ajouté sans changement de code, et
un serveur déjà connu redéfini en réutilisant son identifiant :
{
"lsp": {
"servers": {
"basedpyright": {
"command": "{project}/.venv/bin/basedpyright-langserver",
"args": ["--stdio"],
"languages": ["python"],
"install": "uv tool install basedpyright"
}
},
"defaults": {"python": "basedpyright"}
}
}
command est obligatoire ; le reste peut être omis. La table intégrée
reste disponible à côté de ces entrées : une configuration muette a donc
quand même des serveurs, et une version qui en apprend un nouveau le
propose sans réécriture de la configuration. Définir un serveur ne change
pas celui qu'un langage utilise par défaut ; c'est lsp.defaults qui en
décide.
Nommer un serveur, et en désigner un dans le projet
L'entrée d'un langage accepte l'identifiant d'un serveur connu de l'éditeur ou une ligne de commande.
Un identifiant apporte avec lui les arguments du serveur. Un langage qui a plusieurs serveurs enregistrés utilise le premier tant que la configuration n'en nomme pas un autre.
Une ligne de commande est exécutée telle quelle, avec deux substitutions :
{project}— la racine du projet sur laquelle l'instance est indexée.{home}— le répertoire personnel de l'utilisateur.
{"lsp": {"defaults":
{"python": "{project}/.venv/bin/basedpyright-langserver --stdio"}}}
C'est ce qu'il faut à un dépôt qui embarque ses propres outils : un
environnement virtuel, une entrée de node_modules/.bin, un serveur
inclus dans le dépôt. La substitution se fait argument par argument après
le découpage de la ligne : un chemin de projet contenant des espaces
reste un seul argument.
La commande d'une entrée s'édite sur place — corriger une coquille ou
ajouter un --stdio manquant se fait dans la ligne elle-même, avec ⏎
ou en cliquant ailleurs, sans supprimer puis recréer. Les changements
s'appliquent aux serveurs démarrés ensuite ; le bouton
Restart Servers Now de l'onglet retire les instances en cours et les
relance sous la nouvelle configuration.
Quand il n'y a pas de serveur
Deux filets de sécurité couvrent le cas sans serveur :
- Le repli ctags. Avec Ctags fallback activé dans
Réglages → Projects (par défaut ou par projet, comme chaque drapeau de
projet), Aller à la Définition est répondu depuis un index
Universal Ctags du projet dès qu'aucun serveur de
langage n'est disponible — et aussi quand un serveur en marche n'a pas
de réponse. L'index se construit au premier usage et se rafraîchit au
fil des sauts ; ctags connaît des noms, pas la sémantique : c'est un
repli, pas un remplacement. Il faut Universal Ctags (
brew install universal-ctags) : lectagslivré par macOS dans/usr/binest un autre programme, bien plus ancien, incapable de produire l'index JSON que ceci lit. Textchum regarde au-delà de celui-ci pour trouver un vrai Universal Ctags plus loin dans lePATH. - Le journal de débogage. Chaque décision sur le chemin de « fichier
ouvert » à « serveur en marche » — la racine de projet résolue, quel
serveur a été choisi et pourquoi, les échecs de lancement avec le
PATHexact consulté et chaque transition d'état — est ajoutée à :
~/Library/Logs/Textchum/lsp.log
La sortie d'erreur propre à chaque serveur (stderr) y est capturée
aussi : un serveur qui sort pendant le démarrage laisse sa plainte au
dossier — une commande privée de son option de transport (le
--stdio de pyright, par exemple) se diagnostique d'un coup d'œil,
et le journal signale explicitement quand une commande personnalisée
omet des arguments que le registre intégré sait requis. Quand un
projet se retrouve mystérieusement sans support de langage, ce
fichier nomme la pièce manquante.
Une cause classique mérite sa note : les applications lancées depuis le
Finder héritaient du PATH minimal de macOS, qui ne contient aucun des
endroits où vivent réellement les serveurs de langage (Homebrew, npm,
cargo, go). Textchum adopte désormais au démarrage le PATH du shell de
connexion — plus quelques répertoires d'outils conventionnels — si bien
qu'un serveur qui fonctionne depuis le terminal fonctionne aussi depuis
le Dock.
Sous le capot
Le client vit dans le noyau, derrière la même frontière que tout le reste : JSON-RPC sur stdio, une poignée de main d'initialisation avant tout trafic de documents, et une synchronisation en document complet (la synchronisation incrémentale est une optimisation ultérieure). Les messages du serveur sont traités hors du fil d'interface et la rejoignent par l'unique canal d'événements du noyau ; un processus serveur bloqué reçoit un délai de grâce borné à la fermeture puis est tué, si bien que quitter Textchum ne peut jamais rester suspendu à un serveur défaillant. Tout le chemin du protocole est exercé en CI contre un serveur scripté.
Les instances prennent aussi soin d'elles-mêmes : un serveur qui plante en cours de session est relancé automatiquement avec recul (1 → 2 → 4 → 8 secondes ; quatre échecs d'affilée et il reste à terre jusqu'à un redémarrage ou un changement de configuration), et une instance dont aucun document ouvert n'a eu besoin depuis cinq minutes est arrêtée — l'ouverture suivante en lance une fraîche.
- Les snippets de la complétion se déplient et se parcourent. Le premier marqueur revient sélectionné, donc taper le remplace ; ⇥ passe au suivant et ⇧⇥ au précédent ; un marqueur écrit plusieurs fois recopie celui qu'on tape. Arriver au bout, appuyer sur ⎋ ou cliquer ailleurs rend les touches.
- Présentation ▸ État des serveurs liste les instances en cours et les transitions récentes de la session, rafraîchi en direct, avec un pointeur vers le journal complet.