Summary of VSCode LSP language server protocol

Why use the Language Server Protocol?

LSP( Language Server Protocol ) Language Server is a special Visual Studio Code extension that provides an editing experience for many programming languages. Using a language server, you can implement autocompletion, error checking (diagnostics), jump to definition, and many other language features supported by VS Code.

However, when implementing support for language features in VS Code, we found three common problems:

First, language servers are often implemented in their native programming languages, which presents a challenge for integrating them with VS Code with the Node.js runtime.

Also, language features can be resource intensive. For example, in order to properly validate files, language servers need to parse large numbers of files, build abstract syntax trees for them and perform static program analysis. These operations may cause a lot of CPU and memory usage, we need to ensure that the performance of VS Code is not affected.

Finally, integrating multiple language tools with multiple code editors can be a huge effort. From a language tools perspective, they need to accommodate code editors with different APIs. From a code editor's point of view, they cannot expect any uniform API from language tools. It ends up being the work and effort it takes to implement a language for Nan editor .MN*M

insert image description here

To solve these problems, Microsoft provides the Language Server Protocol (Language Server Protocol) intended to provide community specifications for language plug-ins and editors. In this way, the language server can be implemented in any language, and the protocol communication also avoids the high overhead of running the plugin in the main process. Moreover, any LSP-compatible language plug-in can be integrated with an LSP-compatible code editor. LSP is a win-win solution for language plug-in developers and third-party editors.

In VS Code, the language server has two parts:

  • Language clients: plain VS Code extensions written in JavaScript/TypeScript. This extension has access to all VS Code namespace APIs.
  • Language Server: A language analysis tool that runs in a separate process.

As mentioned above, running the language server in a separate process has two advantages:

  • The analysis tool can be implemented in any language as long as it can communicate with the language client according to the language server protocol.
  • Since language analysis tools are typically CPU and memory intensive, running them in separate processes avoids the performance cost.

First, understand the programming language extension

What programming language-related extensions can be done

Let's look at a picture first to see which programming language extensions vscode supports.
insert image description here
First, we add support for language configuration under contributes under package.json:

"languages": [{
    
    
     "id": "basic",
     "extensions": [
         ".bas" // 自定义语言扩展名
     ],
     "configuration": "./language-configuration.json"
 }

note

Use // for single-line comments and /**/ for multi-line comments. Let's write language-configuation.json like this:

"comments": {
    
    
     "lineComment": "//",
     "blockComment": [
         "/*",
         "*/"
     ]
 }

After definition, we can use Ctrl+K (Windows) or Cmd-K (Mac) to trigger opening or closing comments

bracket matching

We pair parentheses and brackets:

"brackets": [
     [
         "[",
         "]"
     ],
     [
         "(",
         ")"
     ],
 ],

Autocompletion of parentheses

You can use the auto-completion function of parentheses to prevent half of the parentheses from being written:

"autoClosingPairs": [
     {
    
    
         "open": "\"",
         "close": "\""
     },
     {
    
    
         "open": "[",
         "close": "]"
     },
     {
    
    
         "open": "(",
         "close": ")"
     },
     {
    
    
         "open": "Sub",
         "close": "End Sub"
     }
 ]

In the above example, inputting a " will fill in the other half". The same is true for other brackets.

Bracket the selected area

After selecting an area, enter half brackets to automatically surround it with a pair of complete brackets, which is called the auto surrounding function.

example:

"surroundingPairs": [
     [
         "[",
         "]"
     ],
     [
         "(",
         ")"
     ],
     [
         "\"",
         "\""
     ],
     [
         "'",
         "'",
     ]
 ],

code folding

When there are more functions and code blocks, it will bring some difficulties to code reading. We can choose to collapse a block of code. This is also an old feature from the days of Vim and emacs.

Let's take folding Sub/End Sub as an example to see how code folding is written:

    "folding": {
    
    
        "markers": {
    
    
            "start": "^\\s*Sub.*",
            "end": "^\\s*End\\s*Sub.*"
        }
    }

Let's take a look at the effect of sub folding:

insert image description here

Diagnostic diagnostic information (implemented by vscode plug-in extension)

An important feature in language extensions is code scanning diagnostics. This diagnostic information is presented with vscode.Diagnostic as the carrier.
Let's take a look at the members of the vscode.Diagnostic class and the relationship with related classes.
insert image description here
From small to large, these classes are:

  • Position: The coordinates of a character positioned on a line
  • Range: Determined by the two Positions of the starting point and the ending point
  • Location: a Range with a URI
  • DiagnosticRelatedInformation: A Location with a message
  • Diagnostic: The body is a message string, a Range and a DiagnosticRelatedInformation.

Constructing a diagnostic message
Let's construct a diagnostic message.

for(var i = 0; i < 10; i ++) {
    
    
  for(var i = 0; i < 10; i ++) {
    
    
    console.log('*')
  }
}

In this example, the loop control variable is reused in the outer loop and the inner loop, causing the outer loop to fail.
The problematic Range is the 9th to 10th characters of line 2. The position starts from 0, so we construct a Range with two Positions as the beginning and the end such as (2,8) to (2,9).

new vscode.Range(
   new vscode.Position(2, 8), new vscode.Position(2, 9),
)

With Range, plus the problem description string, and the serious program of the problem, a Diagnostic can be constructed.

 let diag1: vscode.Diagnostic = new vscode.Diagnostic(
      new vscode.Range(
          new vscode.Position(2, 8), new vscode.Position(2, 9),
      ),
      '循环变量重复赋值',
      vscode.DiagnosticSeverity.Hint,
  )

Diagnosis-related information
As mentioned in the previous section, there are three items of Range, message, and severity, and a Diagnostic information can be constructed.

In addition, some advanced information can also be set.
The first is the source, such as from a certain version of eslint, using certain rules and the like. This can be written to the source property of Diagnostic.

diag1.source = '某某规则';

The second is the error code, which is helpful for classification and query. This is represented by the code attribute, which can be either a number or a string.

diag1.code = 401;

The third is related information. For the example in the previous section, we said that i has been assigned, so we can further tell the developer where it has been assigned. So there must be a uri, the address where the code can be found. There is also a Range to tell the specific location in the uri. As mentioned earlier, this is a vscode.Location structure.

    diag1.relatedInformation = [new vscode.DiagnosticRelatedInformation(
        new vscode.Location(document.uri,
            new vscode.Range(new vscode.Position(2, 4), new vscode.Position(2, 5))),
        '第一次赋值')];

Let's put them together and give an error message for the above test.js. The main thing is to write the above prompt information into the DiagnosticCollection passed in.

import * as vscode from 'vscode';
import * as path from 'path';

export function updateDiags(document: vscode.TextDocument,
    collection: vscode.DiagnosticCollection): void {
    
    
    let diag1: vscode.Diagnostic = new vscode.Diagnostic(
        new vscode.Range(
            new vscode.Position(2, 8), new vscode.Position(2, 9),
        ),
        '循环变量重复赋值',
        vscode.DiagnosticSeverity.Hint,
    );
    diag1.source = 'basic-lint';
    diag1.relatedInformation = [new vscode.DiagnosticRelatedInformation(
        new vscode.Location(document.uri,
            new vscode.Range(new vscode.Position(2, 4), new vscode.Position(2, 5))),
        '第一次赋值')];
    diag1.code = 102;

    if (document && path.basename(document.uri.fsPath) === 'test.js') {
    
    
        collection.set(document.uri, [diag1]);
    } else {
    
    
        collection.clear();
    }
}

Events that trigger diagnostic information
Next, we add a call to the updateDiags function we just wrote in the activate function of the plugin.

	const diag_coll = vscode.languages.createDiagnosticCollection('basic-lint-1');

	if (vscode.window.activeTextEditor) {
    
    
		diag.updateDiags(vscode.window.activeTextEditor.document, diag_coll);
	}

	context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor(
		(e: vscode.TextEditor | undefined) => {
    
    
			if (e !== undefined) {
    
    
				diag.updateDiags(e.document, diag_coll);
			}
		}));

Run it, open test.bas in the newly started vscode, and then edit the code arbitrarily at the end, and activate things to trigger. The running interface is as follows:
insert image description here

Diagnostic information (implemented in LSP mode)

server code

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.
	const settings = await getDocumentSettings(textDocument.uri);

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

	let problems = 0;
	const diagnostics: Diagnostic[] = [];
	while ((m = pattern.exec(text)) && problems < settings.maxNumberOfProblems) {
    
    
		problems++;
		const 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 VSCode.
	connection.sendDiagnostics({
    
     uri: textDocument.uri, diagnostics });
}

main methodconnection.sendDiagnostics

client-side code

/* --------------------------------------------------------------------------------------------
 * 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
	const 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
	const 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
	const serverOptions: ServerOptions = {
    
    
		run: {
    
     module: serverModule, transport: TransportKind.ipc },
		debug: {
    
    
			module: serverModule,
			transport: TransportKind.ipc,
			options: debugOptions
		}
	};

	// Options to control the language client
	const 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();
}

Two, syntax highlighting

vscode extension highlighting

VS Code's tokenization engine is powered by TextMate syntax . TextMate syntax is a structured collection of regular expressions and is written as a plist (XML) or JSON file. VS Code extensions can contribute grammars through the grammars contribution point.

The TextMate tokenization engine runs in the same process as the renderer, and the tokens are updated with user input. Tags are used for syntax highlighting and also for categorizing source code into comment, string, regex regions.

insert image description here
insert image description here
For example, we define the file name as fwhf(arbitrary).
insert image description here
package.json Detailed introduction
insert image description hereHighlight configuration JSON detailed introduction, more visible explanation of JSON configuration items in the file, or click here
insert image description here
Debugging
insert image description here
reference: https://blog.csdn.net/ qq_42231248/article/details/129683141?spm=1001.2014.3001.5502

https://code.visualstudio.com/api/language-extensions/semantic-highlight-guide

https://code.visualstudio.com/api/language-extensions/programmatic-language-features

https://code.visualstudio.com/api/language-extensions/language-server-extension-guide

DocumentSemanticTokensProvider self-segmentation highlight

Introduction
"Sematic Tokens Provider" is a built-in object protocol of vscode. It needs to scan the content of the code file by itself, and then return the sequence of semantic tokens in the form of an integer array, telling vscode which line, column, and length of the file are What type of token is it.

Note that the scanning in TextMate is engine-driven, and the matching rules are line by line, while the scanning rules and matching rules in the "Sematic Tokens Provider" scenario are all implemented by the plug-in developer themselves, which increases flexibility but also reduces the relative development cost. higher.

In terms of implementation, "Sematic Tokens Provider" is vscode.DocumentSemanticTokensProviderdefined by the interface, and developers can implement two methods as needed:

provideDocumentSemanticTokens : Full analysis of code file semantics
provideDocumentSemanticTokensEdits : Incremental analysis of the semantics of the module being edited
Let's look at a complete example:

import * as vscode from 'vscode';

const tokenTypes = ['class', 'interface', 'enum', 'function', 'variable'];
const tokenModifiers = ['declaration', 'documentation'];
const legend = new vscode.SemanticTokensLegend(tokenTypes, tokenModifiers);

const provider: vscode.DocumentSemanticTokensProvider = {
    
    
  provideDocumentSemanticTokens(
    document: vscode.TextDocument
  ): vscode.ProviderResult<vscode.SemanticTokens> {
    
    
    const tokensBuilder = new vscode.SemanticTokensBuilder(legend);
    tokensBuilder.push(      
      new vscode.Range(new vscode.Position(0, 3), new vscode.Position(0, 8)),
      tokenTypes[0],
      [tokenModifiers[0]]
    );
    return tokensBuilder.build();
  }
};

const selector = {
    
     language: 'javascript', scheme: 'file' };

vscode.languages.registerDocumentSemanticTokensProvider(selector, provider, legend);

I believe that most readers will feel unfamiliar with this code. After thinking about it for a long time, I think it is easier to understand from the perspective of function output, that is, tokensBuilder.build() in line 17 of the above example code.

We can define the participle color by ourselves

"semanticTokenColors": {
    
    
"userName": "#2E8B57",
	"companyName": "#2E8B57",
	"court": "#6495ED",
	"lawFirm": "#4876FF",
	"law": "#FF8247",
	"time": "#EEB422",
	// "address:lawdawn": "#54a15a"

},

insert image description here

3. Build a language server from scratch

The directory structure
insert image description here
is divided into clientend and serverend

Main function realization: extract color

package.json

 "activationEvents": [
    "onLanguage:plaintext"
  ],
  // "main": "./client/dist/browserClientMain", //桌面端
  "browser": "./client/dist/browserClientMain", // 浏览器端
  "contributes": {
    
    
    "configuration": [
      {
    
    
        "order": 22,
        "id": "lsp-web-extension-sample",
        "title": "lsp-web-extension-sample",
        "properties": {
    
    
          "lsp-web-extension-sample.trace.server": {
    
    
            "type": "string",
            "scope": "window",
            "enum": [
              "off",
              "messages",
              "verbose"
            ],
            "default": "verbose",
            "description": "Traces the communication between VS Code and the lsp-web-extension-sample language server."
          }
        }
      }
    ]
  },
  • main: desktop entry
  • browser: browser-side entry

server/src/browserServerMain.ts

/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
import {
    
     createConnection, BrowserMessageReader, BrowserMessageWriter } from 'vscode-languageserver/browser';

import {
    
     Color, ColorInformation, Range, InitializeParams, InitializeResult, ServerCapabilities, TextDocuments, ColorPresentation, TextEdit, TextDocumentIdentifier } from 'vscode-languageserver';
import {
    
     TextDocument } from 'vscode-languageserver-textdocument';


console.log('running server lsp-web-extension-sample');

/* browser specific setup code */

const messageReader = new BrowserMessageReader(self);
const messageWriter = new BrowserMessageWriter(self);

const connection = createConnection(messageReader, messageWriter);

/* from here on, all code is non-browser specific and could be shared with a regular extension */

connection.onInitialize((params: InitializeParams): InitializeResult => {
    
    
	const capabilities: ServerCapabilities = {
    
    
		colorProvider: {
    
    } // provide a color providr
	};
	return {
    
     capabilities };
});

// Track open, change and close text document events
const documents = new TextDocuments(TextDocument);
documents.listen(connection);

// Register providers
connection.onDocumentColor(params => getColorInformation(params.textDocument));
connection.onColorPresentation(params => getColorPresentation(params.color, params.range));

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


const colorRegExp = /#([0-9A-Fa-f]{6})/g;

function getColorInformation(textDocument: TextDocumentIdentifier) {
    
    
	console.log(111); 
	const colorInfos: ColorInformation[] = [];

	const document = documents.get(textDocument.uri);
	if (document) {
    
    
		const text = document.getText();

		colorRegExp.lastIndex = 0;
		let match;
		while ((match = colorRegExp.exec(text)) != null) {
    
    
			console.log('match->', match)
			const offset = match.index;
			const length = match[0].length;

			const range = Range.create(document.positionAt(offset), document.positionAt(offset + length));
			const color = parseColor(text, offset);
			console.log('color-->', color)
			colorInfos.push({
    
     color, range });
		}
	}

	return colorInfos;
}

function getColorPresentation(color: Color, range: Range) {
    
    
	console.log(22)
	const result: ColorPresentation[] = [];
	const red256 = Math.round(color.red * 255), green256 = Math.round(color.green * 255), blue256 = Math.round(color.blue * 255);

	function toTwoDigitHex(n: number): string {
    
    
		const r = n.toString(16);
		return r.length !== 2 ? '0' + r : r;
	}

	const label = `#${
      
      toTwoDigitHex(red256)}${
      
      toTwoDigitHex(green256)}${
      
      toTwoDigitHex(blue256)}`;
	result.push({
    
     label: label, textEdit: TextEdit.replace(range, label) });

	return result;
}


const enum CharCode {
    
    
	Digit0 = 48,
	Digit9 = 57,

	A = 65,
	F = 70,

	a = 97,
	f = 102,
}

function parseHexDigit(charCode: CharCode): number {
    
    
	if (charCode >= CharCode.Digit0 && charCode <= CharCode.Digit9) {
    
    
		return charCode - CharCode.Digit0;
	}
	if (charCode >= CharCode.A && charCode <= CharCode.F) {
    
    
		return charCode - CharCode.A + 10;
	}
	if (charCode >= CharCode.a && charCode <= CharCode.f) {
    
    
		return charCode - CharCode.a + 10;
	}
	return 0;
}

function parseColor(content: string, offset: number): Color {
    
    
	const r = (16 * parseHexDigit(content.charCodeAt(offset + 1)) + parseHexDigit(content.charCodeAt(offset + 2))) / 255;
	const g = (16 * parseHexDigit(content.charCodeAt(offset + 3)) + parseHexDigit(content.charCodeAt(offset + 4))) / 255;
	const b = (16 * parseHexDigit(content.charCodeAt(offset + 5)) + parseHexDigit(content.charCodeAt(offset + 6))) / 255;
	return Color.create(r, g, b, 1);
}

client/src/browserClientMain.ts

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

import {
    
     ExtensionContext, Uri } from 'vscode';
import {
    
     LanguageClientOptions } from 'vscode-languageclient';

import {
    
     LanguageClient } from 'vscode-languageclient/browser';

// this method is called when vs code is activated
export function activate(context: ExtensionContext) {
    
    

	console.log('lsp-web-extension-sample activated!');

	/* 
	 * all except the code to create the language client in not browser specific
	 * and couuld be shared with a regular (Node) extension
	 */
	const documentSelector = [{
    
     language: 'plaintext' }];

	// Options to control the language client
	const clientOptions: LanguageClientOptions = {
    
    
		documentSelector,
		synchronize: {
    
    },
		initializationOptions: {
    
    }
	};

	const client = createWorkerLanguageClient(context, clientOptions);

	const disposable = client.start();
	context.subscriptions.push(disposable);

	client.onReady().then(() => {
    
    
		console.log('lsp-web-extension-sample server is ready');
	});
}

function createWorkerLanguageClient(context: ExtensionContext, clientOptions: LanguageClientOptions) {
    
    
	// Create a worker. The worker main file implements the language server.
	const serverMain = Uri.joinPath(context.extensionUri, 'server/dist/browserServerMain.js');
	const worker = new Worker(serverMain.toString());

	// create the language server client to communicate with the server running in the worker
	return new LanguageClient('lsp-web-extension-sample', 'LSP Web Extension Sample', clientOptions, worker);
}

Debugging:
insert image description here
insert image description here
The official plug-in repository gives many examples, you can download and try it out
Address: https://github.com/microsoft/vscode-extension-samples

There are great gods on the Internet who have summarized the LSP API: https://vimsky.com/examples/detail/typescript-ex-vscode-languageserver-IConnection-onDocumentFormatting-method.html

Reference: https://code.visualstudio.com/api/language-extensions/language-server-extension-guide

https://code.visualstudio.com/api/language-extensions/syntax-highlight-guide

https://cloud.tencent.com/developer/article/1841066

Guess you like

Origin blog.csdn.net/woyebuzhidao321/article/details/122004898