The introduction

As mentioned in the previous VS Code plug-in development tutorial (10), we can directly use the language. * interface to provide support for Language Features, while Language server extension is another approach. The main contents of this article are as follows:

  • Advantages of a language server
  • usingMicrosoft/vscode-languageserver-nodeThe library implements a simple language server
  • How do I run, debug, and test the language server
  • Gives some advanced topics on language servers

Why do we need a language server

The language server is a special part of VS Code’s plug-in architecture. It handles editing in different programming languages with features such as auto-completion, error notification, jump definition, and so on. In general, if we want to implement the above language features, we need to consider three things.

  • Language server itself has its own implementation architecture and implementation mode, how toVS CodeCoordination is a problem
  • Some language features require cross-file analysis, which will consume a lot of CPU and memory. How to support language features without affecting themVS CodeThe normal use of is a problem
  • Our support for languages is based on the editor. When we implement support for one language, we naturally want to use it in more editors. How to better reuse across editors is a problem, otherwisemLanguages,nThe type editor will causem*nThis is not what we had hoped for

VS Code’s solution to this problem is the Language Server Protocol (LSP), which standardizes the implementation of Language features and the communication between the editor. Support for Language features can be implemented in any Language and run in a separate process. The VS Code process is not affected, and the communication protocol with the editor is standardized so it can be easily ported to other editors

Implement a language server

An overview of

A language server in VS Code has two parts:

  • Language Client (hereinafter referred to as Language ClientLC) : OneJavaScriptorTypeScriptWrite theVS CodePlugins that can access all of themVS Code APIIs responsible for starting the language server
  • Language Server (hereinafter referred to as Language Server)LS) : a language analyzer that provides the information needed to support language features. It runs in a separate process and can be developed in any programming language.

Take HTML language service and PHP language service for example, HTML LC and PHP LC instantiate their LS respectively, LS and LC communicate with each other through LSP. HTML LS is written in TypeScript language, while PHP LS is written in PHP language.

An example of a language server plug-in that processes plain text files

We hope that the language server that handles plain text has the function of code auto-completion and error diagnosis. We name the project lsp-sample and the directory structure of the code is as follows:

. ├ ─ ─ the client// Language Client│ ├─ SRC │ ├─ test// End to End tests for Language Client / Server│ │ └ ─ ─ the extension. Ts// Language Client entry point├ ─ ─ package. Json// The extension manifest└ ─ ─ server// Language Server└ ─ ─ the SRC └ ─ ─ for server ts// Language Server entry point
Copy the code

LC implementation

First take a look at the /package.json file for the entire plugin:

{
	"name": "lsp-sample"."description": "A language server example"."author": "Microsoft Corporation"."license": "MIT"."version": "1.0.0"."repository": {
		"type": "git"."url": "https://github.com/Microsoft/vscode-extension-samples"
	},
	"publisher": "vscode-samples"."categories": []."keywords": [
		"multi-root ready"]."engines": {
		"vscode": "^ 1.43.0"
	},
	"activationEvents": [
		"onLanguage:plaintext"]."main": "./client/out/extension"."contributes": {
		"configuration": {
			"type": "object"."title": "Example configuration"."properties": {
				"languageServerExample.maxNumberOfProblems": {
					"scope": "resource"."type": "number"."default": 100."description": "Controls the maximum number of problems produced by the server."
				},
				"languageServerExample.trace.server": {
					"scope": "window"."type": "string"."enum": [
						"off"."messages"."verbose"]."default": "off"."description": "Traces the communication between VS Code and the language server."}}}},"scripts": {
		"vscode:prepublish": "npm run compile"."compile": "tsc -b"."watch": "tsc -b -w"."postinstall": "cd client && npm install && cd .. /server && npm install && cd .."."test": "sh ./scripts/e2e.sh"
	},
	"devDependencies": {
		"@types/mocha": "^ 8.2.2"."@types/node": "^ 12.12.0"."@typescript-eslint/eslint-plugin": "^ 4.23.0"."@typescript-eslint/parser": "^ 4.23.0"."eslint": "^ 7.26.0"."mocha": "^ 8.3.2"."typescript": "^ holdings"}}Copy the code

ActivationEvents -onLanguage:plaintext, this Code tells VS Code to activate the plug-in onLanguage event when the plaintext file is opened. The onLanguage event accepts a language marker, which in this case is plaintext. Each language has its own markup notation, which is case sensitive, and you can find all the Known language identifiers in the Known language Identifiers. If you want to create your own new language, you can configure it in package.json:

{
    "contributes": {
        "languages": [{
            "id": "python"."extensions": [".py"]."aliases": ["Python"."py"]."filenames": []."firstLine": "^ #! /.*\\bpython[0-9.-]*\\b"."configuration": "./language-configuration.json"}}}]Copy the code

Now look at the Configuration section:

"configuration": {
    "type": "object"."title": "Example configuration"."properties": {
        "languageServerExample.maxNumberOfProblems": {
            "scope": "resource"."type": "number"."default": 100."description": "Controls the maximum number of problems produced by the server."}}}Copy the code

This part of the configuration will be used in LS, mainly to configure LS parameters

The source code for LC is as follows:

/* -------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. * ------------------------------------------------------------------------------------------ */

import * as path from 'path';
import { workspace, ExtensionContext } from 'vscode';

import {
	LanguageClient,
	LanguageClientOptions,
	ServerOptions,
	TransportKind
} from 'vscode-languageclient/node';

let client: LanguageClient;

export function activate(context: ExtensionContext) {
	// The server is implemented in node
	let serverModule = context.asAbsolutePath(
		path.join('server'.'out'.'server.js'));// The debug options for the server
	// --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging
	let debugOptions = { execArgv: ['--nolazy'.'--inspect=6009']};// If the extension is launched in debug mode then the debug server options are used
	// Otherwise the run options are used
	let serverOptions: ServerOptions = {
		run: { module: serverModule, transport: TransportKind.ipc },
		debug: {
			module: serverModule,
			transport: TransportKind.ipc,
			options: debugOptions
		}
	};

	// Options to control the language client
	let clientOptions: LanguageClientOptions = {
		// Register the server for plain text documents
		documentSelector: [{ scheme: 'file'.language: 'plaintext'}].synchronize: {
			// Notify the server about file changes to '.clientrc files contained in the workspace
			fileEvents: workspace.createFileSystemWatcher('**/.clientrc')}};// Create the language client and start the client.
	client = new LanguageClient(
		'languageServerExample'.'Language Server Example',
		serverOptions,
		clientOptions
	);

	// Start the client. This will also launch the server
	client.start();
}

export function deactivate() :Thenable<void> | undefined {
	if(! client) {return undefined;
	}
	return client.stop();
}
Copy the code

LS implementation

In this example, LS is written in typescript and runs in a Node.js environment. The advantage of this choice is that VS Code gives us a Node.js environment to run in without worrying about LS running. The source code for LS is in package.json, which refers to two code libraries:

"dependencies": {
    "vscode-languageserver": "^ 7.0.0." "."vscode-languageserver-textdocument": "^" 1.0.1
}
Copy the code

Here is an LS Code implementation that uses the text document manager to take care of file content synchronization between the server and VS Code

import {
    createConnection,
    TextDocuments,
    Diagnostic,
    DiagnosticSeverity,
    ProposedFeatures,
    InitializeParams,
    DidChangeConfigurationNotification,
    CompletionItem,
    CompletionItemKind,
    TextDocumentPositionParams,
    TextDocumentSyncKind,
    InitializeResult
} from 'vscode-languageserver/node';

import {
    TextDocument
} from 'vscode-languageserver-textdocument';

// Create a connection for the server, using Node's IPC as a transport.
// Also include all preview / proposed LSP features.
let connection = createConnection(ProposedFeatures.all);

// Create a simple text document manager.
let documents: TextDocuments < TextDocument > = new TextDocuments(TextDocument);

let hasConfigurationCapability: boolean = false;
let hasWorkspaceFolderCapability: boolean = false;
let hasDiagnosticRelatedInformationCapability: boolean = false;

connection.onInitialize((params: InitializeParams) = > {
    let capabilities = params.capabilities;

    // Does the client support the `workspace/configuration` request?
    // If not, we fall back using global settings.hasConfigurationCapability = !! ( capabilities.workspace && !! capabilities.workspace.configuration ); hasWorkspaceFolderCapability = !! ( capabilities.workspace && !! capabilities.workspace.workspaceFolders ); hasDiagnosticRelatedInformationCapability = !! ( capabilities.textDocument && capabilities.textDocument.publishDiagnostics && capabilities.textDocument.publishDiagnostics.relatedInformation );const result: InitializeResult = {
        capabilities: {
            textDocumentSync: TextDocumentSyncKind.Incremental,
            // Tell the client that this server supports code completion.
            completionProvider: {
                resolveProvider: true}}};if (hasWorkspaceFolderCapability) {
        result.capabilities.workspace = {
            workspaceFolders: {
                supported: true}}; }return result;
});

connection.onInitialized(() = > {
    if (hasConfigurationCapability) {
        // Register for all configuration changes.
        connection.client.register(DidChangeConfigurationNotification.type, undefined);
    }
    if (hasWorkspaceFolderCapability) {
        connection.workspace.onDidChangeWorkspaceFolders(_event= > {
            connection.console.log('Workspace folder change event received.'); }); }});// The example settings
interface ExampleSettings {
    maxNumberOfProblems: number;
}

// The global settings, used when the `workspace/configuration` request is not supported by the client.
// Please note that this is not the case when using this server with the client provided in this example
// but could happen with other clients.
const defaultSettings: ExampleSettings = {
    maxNumberOfProblems: 1000
};
let globalSettings: ExampleSettings = defaultSettings;

// Cache the settings of all open documents
let documentSettings: Map < string, Thenable < ExampleSettings >> = new Map(a); connection.onDidChangeConfiguration(change= > {
    if (hasConfigurationCapability) {
        // Reset all cached document settings
        documentSettings.clear();
    } else {
        globalSettings = < ExampleSettings > (
            (change.settings.languageServerExample || defaultSettings)
        );
    }

    // Revalidate all open text documents
    documents.all().forEach(validateTextDocument);
});

function getDocumentSettings(resource: string) :Thenable < ExampleSettings > {
    if(! hasConfigurationCapability) {return Promise.resolve(globalSettings);
    }
    let result = documentSettings.get(resource);
    if(! result) { result = connection.workspace.getConfiguration({scopeUri: resource,
            section: 'languageServerExample'
        });
        documentSettings.set(resource, result);
    }
    return result;
}

// Only keep settings for open documents
documents.onDidClose(e= > {
    documentSettings.delete(e.document.uri);
});

// The content of a text document has changed. This event is emitted
// when the text document first opened or when its content has changed.
documents.onDidChangeContent(change= > {
    validateTextDocument(change.document);
});

async function validateTextDocument(textDocument: TextDocument) :Promise < void > {
    // In this simple example we get the settings for every validate run.
    let settings = await getDocumentSettings(textDocument.uri);

    // The validator creates diagnostics for all uppercase words length 2 and more
    let text = textDocument.getText();
    let pattern = /\b[A-Z]{2,}\b/g;
    let m: RegExpExecArray | null;

    let problems = 0;
    let diagnostics: Diagnostic[] = [];
    while ((m = pattern.exec(text)) && problems < settings.maxNumberOfProblems) {
        problems++;
        let diagnostic: Diagnostic = {
            severity: DiagnosticSeverity.Warning,
            range: {
                start: textDocument.positionAt(m.index),
                end: textDocument.positionAt(m.index + m[0].length)
            },
            message: `${m[0]} is all uppercase.`.source: 'ex'
        };
        if (hasDiagnosticRelatedInformationCapability) {
            diagnostic.relatedInformation = [{
                    location: {
                        uri: textDocument.uri,
                        range: Object.assign({}, diagnostic.range)
                    },
                    message: 'Spelling matters'
                },
                {
                    location: {
                        uri: textDocument.uri,
                        range: Object.assign({}, diagnostic.range)
                    },
                    message: 'Particularly for names'}]; } diagnostics.push(diagnostic); }// Send the computed diagnostics to VS Code.
    connection.sendDiagnostics({
        uri: textDocument.uri,
        diagnostics
    });
}

connection.onDidChangeWatchedFiles(_change= > {
    // Monitored files have change in VS Code
    connection.console.log('We received a file change event');
});

// This handler provides the initial list of the completion items.
connection.onCompletion(
    (_textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
        // The pass parameter contains the position of the text document in
        // which code complete got requested. For the example we ignore this
        // info and always provide the same completion items.
        return [{
                label: 'TypeScript'.kind: CompletionItemKind.Text,
                data: 1
            },
            {
                label: 'JavaScript'.kind: CompletionItemKind.Text,
                data: 2}]; });// This handler resolves additional information for the item selected in
// the completion list.
connection.onCompletionResolve(
    (item: CompletionItem): CompletionItem= > {
        if (item.data === 1) {
            item.detail = 'TypeScript details';
            item.documentation = 'TypeScript documentation';
        } else if (item.data === 2) {
            item.detail = 'JavaScript details';
            item.documentation = 'JavaScript documentation';
        }
        returnitem; });// Make the text document manager listen on the connection
// for open, change and close text document events
documents.listen(connection);

// Listen on the connection
connection.listen();
Copy the code

In order to achieve the document error diagnostic function, through our registration documents, determine to pure onDidChangeContent change the paper documents and do check. After starting the above plug-in, we create a file called test.txt:

TypeScript lets you write JavaScript the way you really want to.
TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.
ANY browser. ANY host. ANY OS. Open Source.
Copy the code

When we open test.txt it looks like this:

LS and LC debugging

For LC, debugging is relatively simple, just like a normal plugin. LS starts with LC, so we need to bind a debugger to it. We bind the debugger to the LS launch Configuration in the Run View.

LS log service

If LC is implemented using VS code-languageclient, you can configure [langId].trace.server to communicate between LC and LS through the LC name channel. For the example above, Is the configuration “languageServerExample. Trace. Server” : “verbose” implementation

LS Read Configuration

The maximum number of problems to report is defined when writing LC. The LS reads this configuration like this:

function getDocumentSettings(resource: string) :Thenable < ExampleSettings > {
    if(! hasConfigurationCapability) {return Promise.resolve(globalSettings);
    }
    let result = documentSettings.get(resource);
    if(! result) { result = connection.workspace.getConfiguration({scopeUri: resource,
            section: 'languageServerExample'
        });
        documentSettings.set(resource, result);
    }
    return result;
}
Copy the code

The user configuration may change, and in order to listen for this change in LS and re-validate when it changes, we need to reuse the validation code and extract the validateTextDocument function:

async function validateTextDocument(textDocument: TextDocument) :Promise < void > {
    // In this simple example we get the settings for every validate run.
    let settings = await getDocumentSettings(textDocument.uri);

    // The validator creates diagnostics for all uppercase words length 2 and more
    let text = textDocument.getText();
    let pattern = /\b[A-Z]{2,}\b/g;
    let m: RegExpExecArray | null;

    let problems = 0;
    let diagnostics: Diagnostic[] = [];
    while ((m = pattern.exec(text)) && problems < settings.maxNumberOfProblems) {
        problems++;
        let diagnostic: Diagnostic = {
            severity: DiagnosticSeverity.Warning,
            range: {
                start: textDocument.positionAt(m.index),
                end: textDocument.positionAt(m.index + m[0].length)
            },
            message: `${m[0]} is all uppercase.`.source: 'ex'
        };
        if (hasDiagnosticRelatedInformationCapability) {
            diagnostic.relatedInformation = [{
                    location: {
                        uri: textDocument.uri,
                        range: Object.assign({}, diagnostic.range)
                    },
                    message: 'Spelling matters'
                },
                {
                    location: {
                        uri: textDocument.uri,
                        range: Object.assign({}, diagnostic.range)
                    },
                    message: 'Particularly for names'}]; } diagnostics.push(diagnostic); }// Send the computed diagnostics to VS Code.
    connection.sendDiagnostics({
        uri: textDocument.uri,
        diagnostics
    });
}
Copy the code

The listening code is as follows:

connection.onDidChangeConfiguration(change= > {
    if (hasConfigurationCapability) {
        // Reset all cached document settings
        documentSettings.clear();
    } else {
        globalSettings = < ExampleSettings > (
            (change.settings.languageServerExample || defaultSettings)
        );
    }

    // Revalidate all open text documents
    documents.all().forEach(validateTextDocument);
});
Copy the code

Start the plugin and change the maximum number of errors to 1.

Other Language features

Detection tools in VS Code are often implemented as LS, such as ESLint and jshint, but LS can also implement other language features, such as Code completion in the example:

// This handler provides the initial list of the completion items.
connection.onCompletion(
    (_textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
        // The pass parameter contains the position of the text document in
        // which code complete got requested. For the example we ignore this
        // info and always provide the same completion items.
        return [{
                label: 'TypeScript'.kind: CompletionItemKind.Text,
                data: 1
            },
            {
                label: 'JavaScript'.kind: CompletionItemKind.Text,
                data: 2}]; });// This handler resolves additional information for the item selected in
// the completion list.
connection.onCompletionResolve(
    (item: CompletionItem): CompletionItem= > {
        if (item.data === 1) {
            item.detail = 'TypeScript details';
            item.documentation = 'TypeScript documentation';
        } else if (item.data === 2) {
            item.detail = 'JavaScript details';
            item.documentation = 'JavaScript documentation';
        }
        returnitem; });Copy the code

Use the data field as the unique identifier of the completion item, which needs to be able to serialize to JSON. In order for code completion to work, we also need to configure it in the onInitialize function:

connection.onInitialize((params): InitializeResult= >{...return {
        capabilities: {...// Tell the client that the server supports code completion
            completionProvider: {
                resolveProvider: true}}}; });Copy the code

The advanced

Incremental document synchronization

The examples in this article use a simple document manager provided by VS Code-Languageserver for synchronization between VS Code and LS, which has two disadvantages:

  • A lot of data is transferred because the entire contents of the text document are repeatedly sent to the server
  • Incremental document updates are not supported, resulting in redundant parsing and syntax tree creation

For this, the implementation should address the synchronization of incremental updates to the document. To do this, you need three hook functions:

  • onDidOpenTextDocument: called when a text document is opened
  • onDidChangeTextDocument: called when the content of a text document changes
  • onDidCloseTextDocument: called when the text document is closed

Here is a simple example:

connection.onInitialize((params): InitializeResult= >{...return {
        capabilities: {
            // Enable incremental document sync
            textDocumentSync: TextDocumentSyncKind.Incremental, ... }}; }); connection.onDidOpenTextDocument((params) = > {
    // A text document was opened in VS Code.
    // params.uri uniquely identifies the document. For documents stored on disk, this is a file URI.
    // params.text the initial full content of the document.
});

connection.onDidChangeTextDocument((params) = > {
    // The content of a text document has change in VS Code.
    // params.uri uniquely identifies the document.
    // params.contentChanges describe the content changes to the document.
});

connection.onDidCloseTextDocument((params) = > {
    // A text document was closed in VS Code.
    // params.uri uniquely identifies the document.
});
Copy the code

Fault-tolerant processing

Most of the time, the code in the editor is in an incomplete state, in a syntactic error state (such as when typing), but we want to still implement language features such as auto-completion, so we need to do compatibility handling for errors. When implementing PHP support, the VS Code team found that the official PHP parser does not support error compatibility, so it cannot be directly used in LS. Therefore, the VS Code team implemented a version of oil-php-parser that supports error compatibility. And accumulated a lot of detail about this HowItWorks, which is very helpful for anyone who wants to develop LS.

Related articles

  • VS Code Plug-in Development Tutorial (1) Overview

  • VS Code plug-in Development Tutorial (2

  • VS Code Plug-in Development Tutorial (3

  • VS Code Plug-in Development Tutorial (4

  • VS Code plug-in development tutorial (5) Using Command

  • VS Code Plugin development Tutorial (6) Color Theme overview

  • VS Code plug-in development tutorial (7) Tree View

  • VS Code Plug-in Development Tutorial (8) Webview

  • Build a Custom Editor

  • VS Code Plug-in Development Tutorial (10

  • Language Server Extension Guide Language Server Extension Guide