Tutoriel sur le protocole du serveur de langage : de VSCode à Vim

Publié: 2022-03-11

Le principal artefact de tout votre travail est très probablement des fichiers de texte brut. Alors pourquoi n'utilisez-vous pas le Bloc-notes pour les créer ?

La coloration syntaxique et le formatage automatique ne sont que la pointe de l'iceberg. Qu'en est-il du linting, de la complétion de code et de la refactorisation semi-automatique ? Ce sont toutes de très bonnes raisons d'utiliser un "vrai" éditeur de code. Celles-ci sont vitales pour notre quotidien, mais comprenons-nous comment elles fonctionnent ?

Dans ce didacticiel Language Server Protocol, nous allons explorer un peu ces questions et découvrir ce qui motive nos éditeurs de texte. En fin de compte, nous implémenterons ensemble un serveur de langage de base avec des exemples de clients pour VSCode, Sublime Text 3 et Vim.

Compilateurs vs services linguistiques

Nous allons ignorer la coloration syntaxique et le formatage pour l'instant, qui sont traités avec une analyse statique - un sujet intéressant en soi - et nous concentrer sur les principaux commentaires que nous obtenons de ces outils. Il existe deux catégories principales : les compilateurs et les services de langage.

Les compilateurs prennent votre code source et recrachent une forme différente. Si le code ne respecte pas les règles du langage, le compilateur renverra des erreurs. Ceux-ci sont assez familiers. Le problème avec ceci est qu'il est généralement assez lent et limité dans sa portée. Que diriez-vous d'offrir de l'aide pendant que vous êtes encore en train de créer le code ?

C'est ce que proposent les services linguistiques. Ils peuvent vous donner un aperçu de votre base de code pendant qu'elle est encore en cours d'élaboration, et probablement beaucoup plus rapidement que la compilation de l'ensemble du projet.

La portée de ces services est variée. Cela peut être quelque chose d'aussi simple que de retourner une liste de tous les symboles du projet, ou quelque chose de complexe comme retourner des étapes pour refactoriser le code. Ces services sont la principale raison pour laquelle nous utilisons nos éditeurs de code. Si nous voulions simplement compiler et voir les erreurs, nous pourrions le faire en quelques frappes. Les services linguistiques nous donnent plus d'informations, et très rapidement.

Miser sur un éditeur de texte pour la programmation

Notez que nous n'avons pas encore appelé d'éditeurs de texte spécifiques. Expliquons pourquoi avec un exemple.

Disons que vous avez développé un nouveau langage de programmation appelé Lapine. C'est un beau langage et le compilateur donne d'excellents messages d'erreur de type Elm. De plus, vous pouvez fournir l'achèvement du code, des références, une aide à la refactorisation et des diagnostics.

Quel éditeur de code/texte supportez-vous en premier ? Et après ça ? Vous avez une bataille difficile à mener pour que les gens l'adoptent, vous voulez donc le rendre aussi simple que possible. Vous ne voulez pas choisir le mauvais éditeur et manquer des utilisateurs. Et si vous gardiez vos distances avec les éditeurs de code et que vous vous concentriez sur votre spécialité, le langage et ses fonctionnalités ?

Serveurs de langue

Entrez les serveurs de langue . Ce sont des outils qui parlent aux clients linguistiques et fournissent les informations que nous avons mentionnées. Ils sont indépendants des éditeurs de texte pour les raisons que nous venons de décrire avec notre situation hypothétique.

Comme d'habitude, une autre couche d'abstraction est exactement ce dont nous avons besoin. Ceux-ci promettent de rompre le couplage étroit des outils linguistiques et des éditeurs de code. Les créateurs de langage peuvent envelopper leurs fonctionnalités dans un serveur une fois, et les éditeurs de code/texte peuvent ajouter de petites extensions pour se transformer en clients. C'est une victoire pour tout le monde. Pour faciliter cela, cependant, nous devons nous mettre d'accord sur la manière dont ces clients et serveurs communiqueront.

Heureusement pour nous, ce n'est pas hypothétique. Microsoft a déjà commencé par définir le Language Server Protocol.

Comme pour la plupart des grandes idées, elle est née de la nécessité plutôt que de la prévoyance. De nombreux éditeurs de code avaient déjà commencé à ajouter la prise en charge de diverses fonctionnalités de langage ; certaines fonctionnalités externalisées à des outils tiers, certaines réalisées sous le capot au sein des éditeurs. Des problèmes d'évolutivité sont apparus et Microsoft a pris l'initiative de diviser les choses. Oui, Microsoft a ouvert la voie pour déplacer ces fonctionnalités hors des éditeurs de code plutôt que de les stocker dans VSCode. Ils auraient pu continuer à construire leur éditeur, verrouiller les utilisateurs, mais ils les ont libérés.

Protocole de serveur de langue

Le Language Server Protocol (LSP) a été défini en 2016 pour aider à séparer les outils linguistiques et les éditeurs. Il y a encore de nombreuses empreintes digitales VSCode dessus, mais c'est une étape majeure dans la direction de l'agnosticisme de l'éditeur. Examinons un peu le protocole.

Les clients et les serveurs (pensez aux éditeurs de code et aux outils linguistiques) communiquent dans de simples messages texte. Ces messages ont des en-têtes de type HTTP, un contenu JSON-RPC et peuvent provenir du client ou du serveur. Le protocole JSON-RPC définit les requêtes, les réponses et les notifications ainsi que quelques règles de base autour d'elles. Une caractéristique clé est qu'il est conçu pour fonctionner de manière asynchrone, de sorte que les clients/serveurs peuvent traiter les messages dans le désordre et avec un certain degré de parallélisme.

En bref, JSON-RPC permet à un client de demander à un autre programme d'exécuter une méthode avec des paramètres et de renvoyer un résultat ou une erreur. LSP s'appuie sur cela et définit les méthodes disponibles, les structures de données attendues et quelques règles supplémentaires concernant les transactions. Par exemple, il y a un processus de prise de contact lorsque le client démarre le serveur.

Le serveur est avec état et n'est destiné qu'à gérer un seul client à la fois. Cependant, il n'y a pas de restrictions explicites sur la communication, de sorte qu'un serveur de langage peut s'exécuter sur une machine différente de celle du client. En pratique, ce serait assez lent pour les commentaires en temps réel, cependant. Les serveurs de langue et les clients fonctionnent avec les mêmes fichiers et sont assez bavards.

Le LSP a une quantité décente de documentation une fois que vous savez ce qu'il faut rechercher. Comme mentionné, une grande partie de ceci est écrite dans le contexte de VSCode, bien que les idées aient une application beaucoup plus large. Par exemple, la spécification du protocole est entièrement écrite en TypeScript. Pour aider les explorateurs qui ne connaissent pas VSCode et TypeScript, voici une introduction.

Types de messages LSP

Il existe de nombreux groupes de messages définis dans le Language Server Protocol. Ils peuvent être grossièrement divisés en « administrateur » et « fonctionnalités linguistiques ». Les messages d'administration contiennent ceux utilisés dans la poignée de main client/serveur, l'ouverture/la modification de fichiers, etc. Il est important de noter que c'est là que les clients et les serveurs partagent les fonctionnalités qu'ils gèrent. Certes, différents langages et outils offrent différentes fonctionnalités. Cela permet également une adoption progressive. Langserver.org nomme une demi-douzaine de fonctionnalités clés que les clients et les serveurs doivent prendre en charge, dont au moins une est requise pour figurer dans la liste.

Les fonctionnalités linguistiques sont ce qui nous intéresse le plus. Parmi celles-ci, il y en a une à signaler spécifiquement : le message de diagnostic. Les diagnostics sont l'une des fonctionnalités clés. Lorsque vous ouvrez un fichier, on suppose généralement qu'il fonctionnera. Votre éditeur devrait vous dire s'il y a quelque chose qui ne va pas avec le fichier. La façon dont cela se produit avec LSP est la suivante :

  1. Le client ouvre le fichier et envoie textDocument/didOpen au serveur.
  2. Le serveur analyse le fichier et envoie la notification textDocument/publishDiagnostics .
  3. Le client analyse les résultats et affiche des indicateurs d'erreur dans l'éditeur.

Il s'agit d'un moyen passif d'obtenir des informations sur vos services linguistiques. Un exemple plus actif serait de trouver toutes les références du symbole sous votre curseur. Cela donnerait quelque chose comme :

  1. Le client envoie textDocument/references au serveur, en spécifiant un emplacement dans un fichier.
  2. Le serveur trouve le symbole, localise les références dans ce fichier et dans d'autres, et répond avec une liste.
  3. Le client affiche les références à l'utilisateur.

Un outil de liste noire

Nous pourrions sûrement creuser dans les spécificités du Language Server Protocol, mais laissons cela aux implémenteurs clients. Pour cimenter l'idée de la séparation de l'éditeur et de l'outil linguistique, nous jouerons le rôle de créateur d'outils.

Nous resterons simples et, au lieu de créer un nouveau langage et de nouvelles fonctionnalités, nous nous en tiendrons aux diagnostics. Les diagnostics conviennent parfaitement : ce ne sont que des avertissements concernant le contenu d'un fichier. Un linter renvoie des diagnostics. Nous allons faire quelque chose de similaire.

Nous allons créer un outil pour nous signaler les mots que nous aimerions éviter. Ensuite, nous fournirons cette fonctionnalité à deux éditeurs de texte différents.

Le serveur de langue

Tout d'abord, l'outil. Nous allons intégrer cela directement dans un serveur de langage. Pour plus de simplicité, il s'agira d'une application Node.js, bien que nous puissions le faire avec n'importe quelle technologie capable d'utiliser des flux pour la lecture et l'écriture.

Voici la logique. Étant donné du texte, cette méthode renvoie un tableau des mots de la liste noire correspondants et les indices où ils ont été trouvés.

 const getBlacklisted = (text) => { const blacklist = [ 'foo', 'bar', 'baz', ] const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi') const results = [] while ((matches = regex.exec(text)) && results.length < 100) { results.push({ value: matches[0], index: matches.index, }) } return results }

Maintenant, faisons-en un serveur.

 const { TextDocuments, createConnection, } = require('vscode-languageserver') const {TextDocument} = require('vscode-languageserver-textdocument') const connection = createConnection() const documents = new TextDocuments(TextDocument) connection.onInitialize(() => ({ capabilities: { textDocumentSync: documents.syncKind, }, })) documents.listen(connection) connection.listen()

Ici, nous utilisons le vscode-languageserver . Le nom est trompeur, car il peut certainement fonctionner en dehors de VSCode. C'est l'une des nombreuses «empreintes digitales» que vous voyez des origines de LSP. vscode-languageserver prend en charge le protocole de niveau inférieur et vous permet de vous concentrer sur les cas d'utilisation. Cet extrait démarre une connexion et la lie à un gestionnaire de documents. Lorsqu'un client se connecte au serveur, le serveur lui indique qu'il souhaite être averti de l'ouverture de documents texte.

On pourrait s'arrêter ici. Il s'agit d'un serveur LSP pleinement fonctionnel, bien qu'inutile. Au lieu de cela, répondons aux modifications de document avec des informations de diagnostic.

 documents.onDidChangeContent(change => { connection.sendDiagnostics({ uri: change.document.uri, diagnostics: getDiagnostics(change.document), }) })

Enfin, nous relions les points entre le document qui a changé, notre logique et la réponse de diagnostic.

 const getDiagnostics = (textDocument) => getBlacklisted(textDocument.getText()) .map(blacklistToDiagnostic(textDocument)) const { DiagnosticSeverity, } = require('vscode-languageserver') const blacklistToDiagnostic = (textDocument) => ({ index, value }) => ({ severity: DiagnosticSeverity.Warning, range: { start: textDocument.positionAt(index), end: textDocument.positionAt(index + value.length), }, message: `${value} is blacklisted.`, source: 'Blacklister', })

Notre charge utile de diagnostic sera le résultat de l'exécution du texte du document via notre fonction, puis mappée au format attendu par le client.

Ce script créera tout cela pour vous.

 curl -o- https://raw.githubusercontent.com/reergymerej/lsp-article-resources/revision-for-6.0.0/blacklist-server-install.sh | bash

Remarque : Si vous n'êtes pas à l'aise avec des étrangers qui ajoutent des exécutables à votre machine, veuillez vérifier la source. Il crée le projet, télécharge index.js et le npm link est fait pour vous.

Sortie de la commande curl ci-dessus, installant le projet pour vous.

Source complète du serveur

La source finale blacklist-server est :

 #!/usr/bin/env node const { DiagnosticSeverity, TextDocuments, createConnection, } = require('vscode-languageserver') const {TextDocument} = require('vscode-languageserver-textdocument') const getBlacklisted = (text) => { const blacklist = [ 'foo', 'bar', 'baz', ] const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi') const results = [] while ((matches = regex.exec(text)) && results.length < 100) { results.push({ value: matches[0], index: matches.index, }) } return results } const blacklistToDiagnostic = (textDocument) => ({ index, value }) => ({ severity: DiagnosticSeverity.Warning, range: { start: textDocument.positionAt(index), end: textDocument.positionAt(index + value.length), }, message: `${value} is blacklisted.`, source: 'Blacklister', }) const getDiagnostics = (textDocument) => getBlacklisted(textDocument.getText()) .map(blacklistToDiagnostic(textDocument)) const connection = createConnection() const documents = new TextDocuments(TextDocument) connection.onInitialize(() => ({ capabilities: { textDocumentSync: documents.syncKind, }, })) documents.onDidChangeContent(change => { connection.sendDiagnostics({ uri: change.document.uri, diagnostics: getDiagnostics(change.document), }) }) documents.listen(connection) connection.listen()

Tutoriel sur le protocole de serveur de langage : temps pour un essai routier

Une fois le projet link , essayez d'exécuter le serveur en spécifiant stdio comme mécanisme de transport :

 blacklist-server --stdio

Il écoute maintenant sur stdio les messages LSP dont nous avons parlé précédemment. Nous pourrions les fournir manuellement, mais créons plutôt un client.

Client de langage : VSCode

Comme cette technologie est issue de VSCode, il semble approprié de commencer par là. Nous allons créer une extension qui créera un client LSP et le connectera au serveur que nous venons de créer.

Il existe plusieurs façons de créer une extension VSCode, notamment en utilisant Yeoman et le générateur approprié, generator-code . Pour plus de simplicité, faisons un exemple simple.

Clonons le passe-partout et installons ses dépendances :

 git clone [email protected]:reergymerej/standalone-vscode-ext.git blacklist-vscode cd blacklist-vscode npm i # or yarn

Ouvrez le répertoire blacklist-vscode dans VSCode.

Appuyez sur F5 pour démarrer une autre instance de VSCode, en déboguant l'extension.

Dans la «console de débogage» de la première instance de VSCode, vous verrez le texte «Regardez, ma. Une extension!"

Deux instances de VSCode. Celui de gauche exécute l'extension blacklist-vscode et affiche sa sortie de console de débogage, et celui de droite est l'hôte de développement d'extension.

Nous avons maintenant une extension VSCode de base fonctionnant sans toutes les cloches et tous les sifflets. Faisons-en un client LSP. Fermez les deux instances de VSCode et depuis le répertoire blacklist-vscode , exécutez :

 npm i vscode-languageclient

Remplacez extension.js par :

 const { LanguageClient } = require('vscode-languageclient') module.exports = { activate(context) { const executable = { command: 'blacklist-server', args: ['--stdio'], } const serverOptions = { run: executable, debug: executable, } const clientOptions = { documentSelector: [{ scheme: 'file', language: 'plaintext', }], } const client = new LanguageClient( 'blacklist-extension-id', 'Blacklister', serverOptions, clientOptions ) context.subscriptions.push(client.start()) }, }

Cela utilise le vscode-languageclient pour créer un client LSP dans VSCode. Contrairement à vscode-languageserver , ceci est étroitement lié à VSCode. En bref, ce que nous faisons dans cette extension est de créer un client et de lui dire d'utiliser le serveur que nous avons créé dans les étapes précédentes. En passant sous silence les spécificités de l'extension VSCode, nous pouvons voir que nous lui disons d'utiliser ce client LSP pour les fichiers en texte brut.

Pour le tester, ouvrez le répertoire blacklist-vscode dans VSCode. Appuyez sur F5 pour démarrer une autre instance, en déboguant l'extension.

Dans la nouvelle instance de VSCode, créez un fichier texte brut et enregistrez-le. Tapez "foo" ou "bar" et attendez un moment. Vous verrez des avertissements indiquant que ceux-ci sont sur liste noire.

La nouvelle instance de VSCode avec test.txt ouvert, affichant "foo" et "bar" avec une erreur soulignée, et un message à propos de chacun dans le volet des problèmes, indiquant qu'ils sont sur la liste noire.

C'est ça! Nous n'avons pas eu à recréer notre logique, juste à coordonner le client et le serveur.

Recommençons pour un autre éditeur, cette fois Sublime Text 3. Le processus sera assez similaire et un peu plus facile.

Client linguistique : texte sublime 3

Tout d'abord, ouvrez ST3 et ouvrez la palette de commandes. Nous avons besoin d'un framework pour faire de l'éditeur un client LSP. Tapez « Package Control : Install Package » et appuyez sur Entrée. Trouvez le package "LSP" et installez-le. Une fois terminé, nous avons la possibilité de spécifier les clients LSP. Il existe de nombreux préréglages, mais nous n'allons pas les utiliser. Nous avons créé le nôtre.

Encore une fois, ouvrez la palette de commandes. Recherchez "Préférences : Paramètres LSP" et appuyez sur Entrée. Cela ouvrira le fichier de configuration, LSP.sublime-settings , pour le package LSP. Pour ajouter un client personnalisé, utilisez la configuration ci-dessous.

 { "clients": { "blacklister": { "command": [ "blacklist-server", "--stdio" ], "enabled": true, "languages": [ { "syntaxes": [ "Plain text" ] } ] } }, "log_debug": true }

Cela peut sembler familier avec l'extension VSCode. Nous avons défini un client, lui avons dit de travailler sur des fichiers en texte brut et spécifié le serveur de langue.

Enregistrez les paramètres, puis créez et enregistrez un fichier texte brut. Tapez "foo" ou "bar" et attendez. Encore une fois, vous verrez des avertissements indiquant que ceux-ci sont sur liste noire. Le traitement, c'est-à-dire la façon dont les messages sont affichés dans l'éditeur, est différent. Cependant, notre fonctionnalité est la même. Nous avons à peine fait quoi que ce soit cette fois pour ajouter un support à l'éditeur.

Langage « Client » : Vim

Si vous n'êtes toujours pas convaincu que cette séparation des préoccupations facilite le partage des fonctionnalités entre les éditeurs de texte, voici les étapes pour ajouter la même fonctionnalité à Vim via Coc.

Ouvrez Vim et tapez :CocConfig , puis ajoutez :

 "languageserver": { "blacklister": { "command": "blacklist-server", "args": ["--stdio"], "filetypes": ["text"] } }

Terminé.

La séparation client-serveur permet aux langues et aux services linguistiques de prospérer

Séparer la responsabilité des services linguistiques des éditeurs de texte dans lesquels ils sont utilisés est clairement une victoire. Il permet aux créateurs de fonctionnalités linguistiques de se concentrer sur leur spécialité et aux créateurs d'éditeurs de faire de même. C'est une idée assez nouvelle, mais l'adoption se répand.

Maintenant que vous avez une base de travail, vous pouvez peut-être trouver un projet et aider à faire avancer cette idée. La guerre des flammes de l'éditeur ne finira jamais, mais ce n'est pas grave. Tant que les capacités linguistiques peuvent exister en dehors d'éditeurs spécifiques, vous êtes libre d'utiliser l'éditeur de votre choix.


Insigne Microsoft Gold Partner.

En tant que Microsoft Gold Partner, Toptal est votre réseau d'élite d'experts Microsoft. Constituez des équipes performantes avec les experts dont vous avez besoin, où que vous soyez et exactement quand vous en avez besoin !