第七期VSCode插件开发手把手实战:从零搭建完整的语言服务器协议工程

2026-05-30阅读 0热度 0
其他

构建完整LSP工程:从零编写VSCode语言服务器插件

前期理论铺垫已经到位,接下来直接动手搭建一个完整的LSP插件,采用客户端–服务器分离架构。整个工作流程分为服务端与客户端两个模块:先完成服务端逻辑,再实现客户端接入,最后将两者集成。

服务端目录结构

先构建服务端代码。从 package.json 入手——微软官方SDK已将大部分底层细节封装,我们只需引入 vscode-languageserver 模块即可。

package.json

{
    "name": "lsp-demo-server",
    "description": "demo language server",
    "version": "1.0.0",
    "author": "Xulun",
    "license": "MIT",
    "engines": {
        "node": "*"
    },
    "repository": {
        "type": "git",
        "url": "git@code.aliyun.com:lusinga/testlsp.git"
    },
    "dependencies": {
        "vscode-languageserver": "^4.1.3"
    },
    "scripts": {}
}

写好 package.json 后,在 server 目录下执行 npm install 即可安装依赖。安装完成后会自动引入以下子模块:vscode-jsonrpcvscode-languageservervscode-languageserver-protocolvscode-languageserver-typesvscode-uri

tsconfig.json

服务端采用TypeScript开发,因此需要配置 tsconfig.json,选项如下:

{
    "compilerOptions": {
        "target": "es6",
        "module": "commonjs",
        "moduleResolution": "node",
        "sourceMap": true,
        "outDir": "out",
        "rootDir": "src",
        "lib": ["es6"]
    },
    "include": ["src"],
    "exclude": ["node_modules", ".vscode-test"]
}

server.ts 核心实现

接下来编写核心文件 server.ts。首先引入 vscode-languageservervscode-jsonrpc 的依赖:

import {
    createConnection,
    TextDocuments,
    TextDocument,
    Diagnostic,
    DiagnosticSeverity,
    ProposedFeatures,
    InitializeParams,
    DidChangeConfigurationNotification,
    CompletionItem,
    CompletionItemKind,
    TextDocumentPositionParams,
    SymbolInformation,
    WorkspaceSymbolParams,
    WorkspaceEdit,
    WorkspaceFolder
} from 'vscode-languageserver';
import { HandlerResult } from 'vscode-jsonrpc';

为了方便调试日志,引入 log4js,先通过 npm i log4js --save 安装,然后初始化:

import { configure, getLogger } from "log4js";

configure({
    appenders: {
        lsp_demo: {
            type: "dateFile",
            filename: "/Users/ziyingliuziying/working/lsp_demo",
            pattern: "yyyy-MM-dd-hh.log",
            alwaysIncludePattern: true,
        },
    },
    categories: { default: { appenders: ["lsp_demo"], level: "debug" } }
});

const logger = getLogger("lsp_demo");

然后通过 createConnection 创建连接:

let connection = createConnection(ProposedFeatures.all);

连接建立后即可处理事件。先注册初始化事件(与第6节介绍的方式一致):

connection.onInitialize((params: InitializeParams) => {
    let capabilities = params.capabilities;
    return {
        capabilities: {
            completionProvider: {
                resolveProvider: true
            }
        }
    };
});

三次握手完成后,在VS Code中弹出一条通知消息:

connection.onInitialized(() => {
    connection.window.showInformationMessage('Hello World! form server side');
});

最后集成第5节学过的代码补全功能:

connection.onCompletion((_textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
    return [
        {
            label: 'TextView',
            kind: CompletionItemKind.Text,
            data: 1
        },
        {
            label: 'Button',
            kind: CompletionItemKind.Text,
            data: 2
        },
        {
            label: 'ListView',
            kind: CompletionItemKind.Text,
            data: 3
        }
    ];
});

connection.onCompletionResolve((item: CompletionItem): CompletionItem => {
    if (item.data === 1) {
        item.detail = 'TextView';
        item.documentation = 'TextView documentation';
    } else if (item.data === 2) {
        item.detail = 'Button';
        item.documentation = 'Ja vaScript documentation';
    } else if (item.data === 3) {
        item.detail = 'ListView';
        item.documentation = 'ListView documentation';
    }
    return item;
});

客户端目录结构

服务端代码基本就绪,接下来开发客户端。

package.json

客户端同样需要先编写 package.json,注意此处依赖的是 vscode-languageclient,切勿与服务端的 vscode-languageserver 混淆。

{
    "name": "lspdemo-client",
    "description": "demo language server client",
    "author": "Xulun",
    "license": "MIT",
    "version": "0.0.1",
    "publisher": "Xulun",
    "repository": {
        "type": "git",
        "url": "git@code.aliyun.com:lusinga/testlsp.git"
    },
    "engines": {
        "vscode": "^1.33.1"
    },
    "scripts": {
        "update-vscode": "vscode-install",
        "postinstall": "vscode-install"
    },
    "dependencies": {
        "path": "^0.12.7",
        "vscode-languageclient": "^4.1.4"
    },
    "devDependencies": {
        "vscode": "^1.1.30"
    }
}

tsconfig.json

客户端与服务端均使用TypeScript,配置基本相同,直接复用:

{
    "compilerOptions": {
        "module": "commonjs",
        "target": "es6",
        "outDir": "out",
        "rootDir": "src",
        "lib": ["es6"],
        "sourceMap": true
    },
    "include": ["src"],
    "exclude": ["node_modules", ".vscode-test"]
}

extension.ts 入口实现

接下来编写 extension.ts。客户端的职责比服务端简单,核心就是启动语言服务器:

// Create the language client and start the client.
client = new LanguageClient(
    'DemoLanguageServer',
    'Demo Language Server',
    serverOptions,
    clientOptions
);
// Start the client. This will also launch the server
client.start();

serverOptions 用于配置服务端参数,其类型定义如下:

export type ServerOptions = Executable
    | { run: Executable; debug: Executable; }
    | { run: NodeModule; debug: NodeModule }
    | NodeModule
    | (() => Thenable);

相关类型的简图:
ServerOptions

具体配置如下:

// 服务端配置
let serverModule = context.asAbsolutePath(path.join('server', 'out', 'server.js'));
let serverOptions: ServerOptions = {
    module: serverModule,
    transport: TransportKind.ipc
};

// 客户端配置
let clientOptions: LanguageClientOptions = {
    // js代码触发事情
    documentSelector: [{ scheme: 'file', language: 'js' }],
};

完整的 extension.ts 代码如下:

import * as path from 'path';
import { workspace, ExtensionContext } from 'vscode';
import {
    LanguageClient,
    LanguageClientOptions,
    ServerOptions,
    TransportKind
} from 'vscode-languageclient';

let client: LanguageClient;

export function activate(context: ExtensionContext) {
    // 服务端配置
    let serverModule = context.asAbsolutePath(path.join('server', 'out', 'server.js'));
    let serverOptions: ServerOptions = {
        module: serverModule,
        transport: TransportKind.ipc
    };

    // 客户端配置
    let clientOptions: LanguageClientOptions = {
        // js代码触发事情
        documentSelector: [{ scheme: 'file', language: 'js' }],
    };

    client = new LanguageClient(
        'DemoLanguageServer',
        'Demo Language Server',
        serverOptions,
        clientOptions
    );

    // 启动客户端,同时启动语言服务器
    client.start();
}

export function deactivate(): Thenable | undefined {
    if (!client) {
        return undefined;
    }
    return client.stop();
}

集成与运行

服务端与客户端均已就绪,现在进行组装。主要涉及插件的 package.json、根目录 tsconfig.json 以及 VS Code 调试配置。

插件配置 - package.json

重点关注入口函数和触发事件:

"activationEvents": ["onLanguage:javascript"],
"main": "./client/out/extension",

完整的 package.json 如下:

{
    "name": "lsp_demo_server",
    "description": "A demo language server",
    "author": "Xulun",
    "license": "MIT",
    "version": "1.0.0",
    "repository": {
        "type": "git",
        "url": "git@code.aliyun.com:lusinga/testlsp.git"
    },
    "publisher": "Xulun",
    "categories": [],
    "keywords": [],
    "engines": {
        "vscode": "^1.33.1"
    },
    "activationEvents": ["onLanguage:javascript"],
    "main": "./client/out/extension",
    "contributes": {},
    "scripts": {
        "vscode:prepublish": "cd client && npm run update-vscode && cd .. && 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": "^5.2.0",
        "@types/node": "^8.0.0",
        "tslint": "^5.11.0",
        "typescript": "^3.1.3"
    }
}

根目录 tsconfig.json

还需一个顶层 tsconfig.json,引用 clientserver 两个子项目:

{
    "compilerOptions": {
        "module": "commonjs",
        "target": "es6",
        "outDir": "out",
        "rootDir": "src",
        "lib": [ "es6" ],
        "sourceMap": true
    },
    "include": ["src"],
    "exclude": ["node_modules",".vscode-test"],
    "references": [
        { "path": "./client" },
        { "path": "./server" }
    ]
}

VS Code 调试配置

截至目前,client、server 及整合代码已完成。接下来在 .vscode 目录中编写两个配置文件,以便调试和运行。

.vscode/launch.json

配置此文件后,即可通过 F5 启动调试。

// 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/out/**/*.js"],
            "preLaunchTask": {
                "type": "npm",
                "script": "watch"
            }
        },
        {
            "type": "node",
            "request": "attach",
            "name": "Attach to Server",
            "port": 6009,
            "restart": true,
            "outFiles": ["${workspaceRoot}/server/out/**/*.js"]
        },
    ],
    "compounds": [
        {
            "name": "Client + Server",
            "configurations": ["Launch Client", "Attach to Server"]
        }
    ]
}

.vscode/tasks.json

配置 npm compile 和 npm watch 两个脚本任务。

{
    "version": "2.0.0",
    "tasks": [
        {
            "type": "npm",
            "script": "compile",
            "group": "build",
            "presentation": {
                "panel": "dedicated",
                "reveal": "never"
            },
            "problemMatcher": ["$tsc"]
        },
        {
            "type": "npm",
            "script": "watch",
            "isBackground": true,
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "presentation": {
                "panel": "dedicated",
                "reveal": "never"
            },
            "problemMatcher": ["$tsc-watch"]
        }
    ]
}

全部配置完成后,在插件根目录执行 npm install,然后在 VS Code 中运行构建命令(macOS 下为 Cmd+Shift+B),即可生成 serverclientout 目录下的 JS 与 source map 文件。此时按下 F5 即可启动运行。

本示例的完整源代码托管于 git@code.aliyun.com:lusinga/testlsp.git,如有需要可自行克隆获取。

免责声明

本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。

相关阅读

更多
欢迎回来 登录或注册后,可保存提示词和历史记录
登录后可同步收藏、历史记录和常用模板
注册即表示同意服务条款与隐私政策