Protocollo di server di linguaggio

Che cos'è il protocollo del server di linguaggio?

Il supporto di funzionalità di modifica avanzata, ad esempio completamento automatico del codice sorgente o Vai a definizione per un linguaggio di programmazione in un editor o un IDE, è tradizionalmente molto impegnativo e richiede molto tempo. In genere richiede la scrittura di un modello di dominio (uno scanner, un parser, un controllo dei tipi, un generatore e altro ancora) nel linguaggio di programmazione dell'editor o dell'IDE. Ad esempio, il plug-in Eclipse CDT, che fornisce il supporto per C/C++ nell'IDE eclipse è scritto in Java perché l'IDE eclipse stesso è scritto in Java. Seguendo questo approccio, significa implementare un modello di dominio C/C++ in TypeScript per Visual Studio Code e un modello di dominio separato in C# per Visual Studio.

La creazione di modelli di dominio specifici del linguaggio è molto più semplice se uno strumento di sviluppo può riutilizzare librerie specifiche del linguaggio esistenti. Tuttavia, queste librerie vengono in genere implementate nel linguaggio di programmazione stesso (ad esempio, i modelli di dominio C/C++ validi vengono implementati in C/C++). L'integrazione di una libreria C/C++ in un editor scritto in TypeScript è tecnicamente possibile, ma difficile da eseguire.

Server di lingua

Un altro approccio consiste nell'eseguire la libreria nel proprio processo e usare la comunicazione tra processi per comunicare con esso. I messaggi inviati in avanti e indietro formano un protocollo. Il protocollo LSP (Language Server Protocol) è il prodotto della standardizzazione dei messaggi scambiati tra uno strumento di sviluppo e un processo del server del linguaggio. L'uso di server linguistici o demoni non è un'idea nuova o nuova. Gli editor come Vim ed Emacs lo hanno fatto per qualche tempo per fornire supporto per il completamento automatico semantico. L'obiettivo del provider di servizi di configurazione locale era quello di semplificare questi tipi di integrazioni e fornire un framework utile per esporre le funzionalità del linguaggio a un'ampia gamma di strumenti.

La presenza di un protocollo comune consente l'integrazione delle funzionalità del linguaggio di programmazione in uno strumento di sviluppo con problemi minimi riutilizzando un'implementazione esistente del modello di dominio del linguaggio. Un back-end del server di linguaggio può essere scritto in PHP, Python o Java e il provider di servizi di configurazione locale consente di integrarlo facilmente in un'ampia gamma di strumenti. Il protocollo funziona a un livello comune di astrazione in modo che uno strumento possa offrire servizi linguistici avanzati senza dover comprendere appieno le sfumature specifiche del modello di dominio sottostante.

Come funziona il provider di servizi di configurazione locale avviato

LSP si è evoluto nel tempo e oggi è alla versione 3.0. È iniziato quando il concetto di server di linguaggio è stato prelevato da OmniSharp per fornire funzionalità di modifica avanzate per C#. Inizialmente, OmniSharp usava il protocollo HTTP con un payload JSON ed è stato integrato in diversi editor, tra cui Visual Studio Code.

Allo stesso tempo, Microsoft ha iniziato a lavorare su un server di linguaggio TypeScript, con l'idea di supportare TypeScript in editor come Emacs e Sublime Text. In questa implementazione un editor comunica tramite stdin/stdout con il processo del server TypeScript e usa un payload JSON ispirato al protocollo del debugger V8 per le richieste e le risposte. Il server TypeScript è stato integrato nel plug-in TypeScript Sublime e VS Code per la modifica avanzata di TypeScript.

Dopo aver integrato due server di linguaggio diversi, il team di VS Code ha iniziato a esplorare un protocollo common language server per editor e IDE. Un protocollo comune consente a un provider di linguaggio di creare un singolo server linguistico che può essere utilizzato da IDE diversi. Un consumer del server di linguaggio deve implementare solo il lato client del protocollo una sola volta. Ciò comporta una situazione win-win sia per il provider di lingue che per il consumer linguistico.

Il protocollo del server di linguaggio è stato avviato con il protocollo usato dal server TypeScript, espandendolo con altre funzionalità del linguaggio ispirato all'API del linguaggio vs Code. Il protocollo è supportato con JSON-RPC per la chiamata remota grazie alla semplicità e alle librerie esistenti.

Il team di VS Code ha creato il prototipo del protocollo implementando diversi server del linguaggio linter che rispondono alle richieste di lint (analisi) di un file e restituiscono un set di avvisi ed errori rilevati. L'obiettivo era quello di eseguire lint di un file mentre l'utente modifica in un documento, il che significa che durante una sessione dell'editor saranno presenti molte richieste di linting. Ha senso mantenere attivo e in esecuzione un server in modo che non sia necessario avviare un nuovo processo di linting per ogni modifica utente. Sono stati implementati diversi server linter, tra cui le estensioni ESLint e TSLint di VS Code. Questi due server linter sono entrambi implementati in TypeScript/JavaScript ed eseguiti in Node.js. Condividono una libreria che implementa la parte client e server del protocollo.

Funzionamento del provider di servizi di configurazione

Un server di linguaggio viene eseguito nel proprio processo e strumenti come Visual Studio o VS Code comunicano con il server usando il protocollo del linguaggio tramite JSON-RPC. Un altro vantaggio del server di linguaggio che opera in un processo dedicato è che vengono evitati problemi di prestazioni correlati a un singolo modello di processo. Il canale di trasporto effettivo può essere stdio, socket, named pipe o node ipc se sia il client che il server vengono scritti in Node.js.

Di seguito è riportato un esempio di come uno strumento e un server di linguaggio comunicano durante una sessione di modifica di routine:

lsp flow diagram

  • L'utente apre un file (detto documento) nello strumento: lo strumento notifica al server di lingua che un documento è aperto ('textDocument/didOpen'). Da ora in poi, la verità sul contenuto del documento non è più nel file system, ma mantenuta dallo strumento in memoria.

  • L'utente apporta modifiche: lo strumento notifica al server la modifica del documento ('textDocument/didChange') e le informazioni semantiche del programma vengono aggiornate dal server di lingua. In questo caso, il server linguistico analizza queste informazioni e invia una notifica allo strumento con gli errori e gli avvisi rilevati ('textDocument/publishDiagnostics').

  • L'utente esegue "Vai alla definizione" su un simbolo nell'editor: lo strumento invia una richiesta "textDocument/definition" con due parametri: (1) l'URI del documento e (2) la posizione di testo da cui è stata avviata la richiesta Vai a definizione al server. Il server risponde con l'URI del documento e la posizione della definizione del simbolo all'interno del documento.

  • L'utente chiude il documento (file): una notifica "textDocument/didClose" viene inviata dallo strumento, informando il server di lingua che il documento non è più in memoria e che il contenuto corrente è ora aggiornato nel file system.

In questo esempio viene illustrato come il protocollo comunica con il server di linguaggio a livello di funzionalità dell'editor, ad esempio "Vai a definizione", "Trova tutti i riferimenti". I tipi di dati usati dal protocollo sono editor o IDE 'tipi di dati' come il documento di testo aperto e la posizione del cursore. I tipi di dati non sono a livello di un modello di dominio del linguaggio di programmazione che in genere fornisce alberi della sintassi astratta e simboli del compilatore (ad esempio, tipi risolti, spazi dei nomi, ...). Questo semplifica significativamente il protocollo.

Ora esaminiamo la richiesta "textDocument/definition" in modo più dettagliato. Di seguito sono riportati i payload che passano tra lo strumento client e il server di linguaggio per la richiesta "Vai a definizione" in un documento C++.

Questa è la richiesta:

{
    "jsonrpc": "2.0",
    "id" : 1,
    "method": "textDocument/definition",
    "params": {
        "textDocument": {
            "uri": "file:///p%3A/mseng/VSCode/Playgrounds/cpp/use.cpp"
        },
        "position": {
            "line": 3,
            "character": 12
        }
    }
}

Questa è la risposta:

{
    "jsonrpc": "2.0",
    "id": "1",
    "result": {
        "uri": "file:///p%3A/mseng/VSCode/Playgrounds/cpp/provide.cpp",
        "range": {
            "start": {
                "line": 0,
                "character": 4
            },
            "end": {
                "line": 0,
                "character": 11
            }
        }
    }
}

In modo retrospettivo, la descrizione dei tipi di dati a livello dell'editor anziché a livello del modello di linguaggio di programmazione è uno dei motivi per il successo del protocollo del server del linguaggio. È molto più semplice standardizzare un URI del documento di testo o una posizione del cursore rispetto alla standardizzazione di un albero della sintassi astratta e dei simboli del compilatore in diversi linguaggi di programmazione.

Quando un utente usa linguaggi diversi, VS Code avvia in genere un server di linguaggio per ogni linguaggio di programmazione. L'esempio seguente illustra una sessione in cui l'utente lavora nei file Java e SASS.

java and sass

Funzionalità

Non tutti i server di linguaggio possono supportare tutte le funzionalità definite dal protocollo. Pertanto, il client e il server annunciano il set di funzionalità supportato tramite "funzionalità". Ad esempio, un server annuncia che può gestire la richiesta 'textDocument/definition', ma potrebbe non gestire la richiesta 'workspace/symbol'. Analogamente, i client possono annunciare che sono in grado di fornire notifiche di salvataggio prima del salvataggio di un documento, in modo che un server possa calcolare le modifiche testuali per formattare automaticamente il documento modificato.

Integrazione di un server di linguaggio

L'effettiva integrazione di un server di linguaggio in uno strumento specifico non è definita dal protocollo del server di linguaggio e viene lasciata agli implementatori dello strumento. Alcuni strumenti integrano in modo generico i server di linguaggio con un'estensione che può avviare e comunicare con qualsiasi tipo di server di linguaggio. Altri, come VS Code, creano un'estensione personalizzata per ogni server di linguaggio, in modo che un'estensione sia ancora in grado di fornire alcune funzionalità del linguaggio personalizzate.

Per semplificare l'implementazione di server e client di linguaggio, sono disponibili librerie o SDK per le parti client e server. Queste librerie vengono fornite per lingue diverse. Ad esempio, è disponibile un modulo npm del client di linguaggio per semplificare l'integrazione di un server di linguaggio in un'estensione vs Code e un altro modulo npm del server di linguaggio per scrivere un server di linguaggio usando Node.js. Questo è l'elenco corrente delle librerie di supporto.

Uso del protocollo del server di linguaggio in Visual Studio

  • Aggiunta di un'estensione Language Server Protocol : informazioni sull'integrazione di un server di linguaggio in Visual Studio.