Language server for vscode plugin development

basic concepts

The language server is divided into a client and a server. When the vscode plug-in is started, it is the client part. The client code will invoke the server and then communicate.

Language servers can be implemented in any language and run in their own process to avoid performance costs because they communicate with the code editor through the Language Server Protocol .

For example, in order to correctly verify a file, the language server needs to parse a large number of files, build an abstract syntax tree for it, and perform static code analysis. These operations may consume a lot of CPU and memory, we need to ensure that the performance of the VSCode code is not affected. So the language server was born.

The language server communicates with the client through the language server, and the language server protocol is adapted to code editors with different apis. You develop a plug-in that can be used in different editors.
Insert picture description here

Development plugin

To develop the plug-in, we need to perform the following operations;

├── client // 语言客户端
│   ├── package.json
│   ├── src
│   │   ├── test // 单元测试文件
│   │   └── extension.js // 语言客户端的入口文件
├── package.json // 插件的描述文件
└── server // 语言服务端
    └── package.json
    └── src
        └── server.js // 语言服务端的入口文件

Understand the two packages:

vscode-languageclient:npm模块,用于从VSCode客户端与VSCode语言服务器通信:
vscode-languageserver:npm模块,用于使用Node.js作为运行时来实现VSCode语言服务器:

Realize code verification in plain text

Create the client code src/extension.js


const path = require("path");
const vscode_1 = require("vscode");
const vscode_languageclient_1 = require("vscode-languageclient");
let client;
function activate(context) {
    
    
    // node 服务器路径
    let serverModule = context.asAbsolutePath(path.join('server', 'out', 'server.js'));
    // --inspect=6009: 开启调试模式
    let debugOptions = {
    
     execArgv: ['--nolazy', '--inspect=6009'] };
    // 如果插件在调试模式下启动,则使用调试服务器选项,否则将使用运行选项
    let serverOptions = {
    
    
        // 运行时的参数
        run: {
    
     module: serverModule, transport: vscode_languageclient_1.TransportKind.ipc },
        // 调试时的参数
        debug: {
    
    
            module: serverModule,
            transport: vscode_languageclient_1.TransportKind.ipc,
            options: debugOptions
        }
    };
    // 语言客户端的一些参数
    let clientOptions = {
    
    
        // 为纯文本文档注册服务器
        documentSelector: [{
    
     scheme: 'file', language: 'plaintext' }],
        synchronize: {
    
    
            // 在“.clientrc文件”的文件更改通知服务器,如果不想校验这个代码可以在这里配置
            fileEvents: vscode_1.workspace.createFileSystemWatcher('**/.clientrc')
        }
    };
    // 创建客户端
    client = new vscode_languageclient_1.LanguageClient('languageServerExample', 'Language Server Example', serverOptions, clientOptions);
    // 启动
    client.start();
}
exports.activate = activate;
function deactivate() {
    
    
    if (!client) {
    
    
        return undefined;
    }
    return client.stop();
}
exports.deactivate = deactivate;

The next server codesrc/server.js


const vscode_languageserver_1 = require("vscode-languageserver");
const vscode_languageserver_textdocument_1 = require("vscode-languageserver-textdocument");
// 为服务创建连接,通过IPC管道与客户端通讯
let connection = vscode_languageserver_1.createConnection(vscode_languageserver_1.ProposedFeatures.all);
// npm模块,用于实现使用Node.js作为运行时的LSP服务器中可用的文本文档:
let documents = new vscode_languageserver_1.TextDocuments(vscode_languageserver_textdocument_1.TextDocument);
let hasConfigurationCapability = false;
let hasWorkspaceFolderCapability = false;
let hasDiagnosticRelatedInformationCapability = false;
connection.onInitialize((params) => {
    
    
    let capabilities = params.capabilities;
    // 客户端支持 `workspace/configuration` 配置?
    // 如果没有支持全局配置
    hasConfigurationCapability = !!(capabilities.workspace && !!capabilities.workspace.configuration);
    hasWorkspaceFolderCapability = !!(capabilities.workspace && !!capabilities.workspace.workspaceFolders);
    hasDiagnosticRelatedInformationCapability = !!(capabilities.textDocument &&
        capabilities.textDocument.publishDiagnostics &&
        capabilities.textDocument.publishDiagnostics.relatedInformation);
    const result = {
    
    
        capabilities: {
    
    
            textDocumentSync: vscode_languageserver_1.TextDocumentSyncKind.Incremental,
            // 告诉客户端支持代码补全
            completionProvider: {
    
    
                resolveProvider: true
            }
        }
    };
    if (hasWorkspaceFolderCapability) {
    
    
        result.capabilities.workspace = {
    
    
            workspaceFolders: {
    
    
                supported: true
            }
        };
    }
    return result;
});
connection.onInitialized(() => {
    
    
    if (hasConfigurationCapability) {
    
    
        // 注册所有配置更改。
        connection.client.register(vscode_languageserver_1.DidChangeConfigurationNotification.type, undefined);
    }
    if (hasWorkspaceFolderCapability) {
    
    
        connection.workspace.onDidChangeWorkspaceFolders(_event => {
    
    
            connection.console.log('Workspace folder change event received.');
        });
    }
});
// 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 = {
    
     maxNumberOfProblems: 1000 };
let globalSettings = defaultSettings;
// Cache the settings of all open documents
let documentSettings = new Map();
connection.onDidChangeConfiguration(change => {
    
    
    if (hasConfigurationCapability) {
    
    
        // Reset all cached document settings
        documentSettings.clear();
    }
    else {
    
    
        globalSettings = ((change.settings.languageServerExample || defaultSettings));
    }
    // Revalidate all open text documents
    documents.all().forEach(validateTextDocument);
});
function getDocumentSettings(resource) {
    
    
    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) {
    
    
    // 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;
    let problems = 0;
    let diagnostics = [];
    while ((m = pattern.exec(text)) && problems < settings.maxNumberOfProblems) {
    
    
        problems++;
        let diagnostic = {
    
    
            severity: vscode_languageserver_1.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 });
}
connection.onDidChangeWatchedFiles(_change => {
    
    
    // Monitored files have change in VSCode
    connection.console.log('We received an file change event');
});
// This handler provides the initial list of the completion items.
connection.onCompletion((_textDocumentPosition) => {
    
    
    // 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: vscode_languageserver_1.CompletionItemKind.Text,
            data: 1
        },
        {
    
    
            label: 'JavaScript',
            kind: vscode_languageserver_1.CompletionItemKind.Text,
            data: 2
        }
    ];
});
// This handler resolves additional information for the item selected in
// the completion list.
connection.onCompletionResolve((item) => {
    
    
    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';
    }
    return item;
});
// 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();

Then configure a few commands .vscode/lunck.json

// A launch configuration that compiles the extension and then opens it inside a new window
{
	"version": "0.2.0",
	"configurations": [
		{
			"type": "extensionHost",
			"request": "launch",
			"name": "Launch Client",
			"runtimeExecutable": "${execPath}",
			"args": ["--extensionDevelopmentPath=${workspaceRoot}"],
			"outFiles": ["${workspaceRoot}/client/src/**/*.js"],
			"preLaunchTask": {
				"type": "npm",
				"script": "watch"
			}
		},
		{
			"type": "node",
			"request": "attach",
			"name": "Attach to Server",
			"port": 6009,
			"restart": true,
			"outFiles": ["${workspaceRoot}/server/src/**/*.js"]
		}
	],
	"compounds": [
		{
			"name": "Client + Server",
			"configurations": ["Launch Client", "Attach to Server"]
		}
	]
}

demo ruins

Guess you like

Origin blog.csdn.net/wu_xianqiang/article/details/107574865