vscode插件开发之语言服务器

基本概念

语言服务器分为客户端和服务端,vscode插件启动的时候启动的是客户端部分,客户端代码里面会唤起服务端,然后进行通讯。

语言服务器可以用任何语言实现,并在自己的进程中运行,以避免性能成本,因为它们通过语言服务器协议(Language Server Protocol )与代码编辑器通信。

例如,为了正确验证一个文件,语言服务器需要解析大量的文件,为它建立抽象语法树,并执行静态代码分析。这些操作可能会占用大量的CPU和内存,我们需要确保VSCode代码的性能不受影响。所以就有了语言服务器的诞生。

语言服务器通过语言服务器与客户端通讯,语言服务器协议是适应具有不同api的代码编辑器。你开发一个插件可以在不同编辑器中使用。
在这里插入图片描述

开发插件

开发插件我们需要进行如下操作;

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

了解两个包:

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

实现纯文本的代码校验工作

创建客户端的代码 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;

接下来来的服务端的代码src/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();

然后配置几个命令 .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地址

猜你喜欢

转载自blog.csdn.net/wu_xianqiang/article/details/107574865