likes
comments
collection
share

基于lsp 增强代码编辑器

作者站长头像
站长
· 阅读数 7

最近因为我们公司的低代码开发平台中的代码编辑器的一些诟病,饱受开发同学吐槽,我们决定基于LSP 方式增强已有的代码编辑器功能,来提升开发同学们的用户体验。学习了一下LSP,整理总结一下如下

1. LSP 基本概念

LSP(Language Server Protocol)是一种语言服务器协议,它定义了一种通用的协议,使得不同的编辑器或IDE可以使用相同的语言服务器进行代码编辑和分析。LSP的主要目的是提高编辑器和IDE的代码智能化和跨平台性。

LSP 的基本工作原理是:编辑器或IDE与语言服务器建立连接后,通过 LSP 协议进行通信。当用户在编辑器中输入代码时,编辑器会将相关信息发送给语言服务器,语言服务器会进行代码分析和补全等操作,并将结果返回给编辑器,编辑器再将结果展示给用户。

使用 LSP 可以实现多种功能,包括代码补全、代码格式化、语法检查、代码重构等。LSP 的优势在于其跨平台性和灵活性,可以让不同的编辑器或IDE使用相同的语言服务器,从而提高开发效率和代码质量。

先基于vscode 我们实现一个简单的TypeScript 语言服务器,我们需要安装 TypeScript 语言服务器和 VS Code 编辑器。

新建 TypeScript 项目 在项目中创建一个名为server.ts的文件

我们实现代码补全

// 导入了一些 LSP 相关的模块,然后创建了一个 `connection` 对象和一个 `documents` 对象。
import {
    createConnection,
    TextDocuments,
    ProposedFeatures,
    InitializeParams,
    CompletionItem,
    CompletionItemKind,
} from 'vscode-languageserver';

// connection对象用于与客户端建立连接,并注册了一些事件处理函数。`documents` 对象用于管理文档,并监听文档变化事件。
const connection = createConnection(ProposedFeatures.all);
const documents = new TextDocuments();

documents.listen(connection);
// 在 `onInitialize` 事件处理函数中,我们返回了一些 LSP 支持的能力,包括文档同步和代码补全。
connection.onInitialize((params: InitializeParams) => {
    return {
        capabilities: {
            textDocumentSync: documents.syncKind,
            completionProvider: {
                resolveProvider: true,
                triggerCharacters: ['.'],
            },
        },
    };
});
// 在 onCompletion 事件处理函数中,我们创建了两个 CompletionItem 对象,并将它们作为代码补全的结果返回。
connection.onCompletion((textDocumentPosition) => {
    const { line, character } = textDocumentPosition.position;
    const document = documents.get(textDocumentPosition.textDocument.uri);

    const completions: CompletionItem[] = [
        {
            label: 'Hello',
            kind: CompletionItemKind.Text,
            data: 1,
        },
        {
            label: 'World',
            kind: CompletionItemKind.Text,
            data: 2,
        },
    ];

    return completions;
});
// 我们调用 `connection.listen()` 方法来启动 LSP 服务器。
connection.listen();

在 VS Code 编辑器中打开 client.ts 文件,并输入以下代码:

console.

当输入 console. 时,编辑器会自动调用 LSP 服务器,展示代码补全结果。

这个示例只是 LSP 的一个简单演示,实际上 LSP 还支持更多的功能和事件,例如代码格式化、语法检查、代码重构等。

除了代码补全功能之外,LSP 还支持以下功能和事件:

  1. 文档同步:当文档发生变化时,LSP 可以自动同步文档内容,以便进行语法检查、代码重构等操作。
  2. 代码格式化:LSP 可以根据编码规范自动格式化代码,以提高代码可读性和一致性。
  3. 语法检查:LSP 可以检查代码中的语法错误和潜在问题,并提供相应的修复建议。
  4. 代码重构:LSP 可以根据代码结构和语义,自动重构代码,以提高代码质量和可维护性。
  5. 调试支持:LSP 可以与调试器集成,提供调试支持,以便更方便地调试代码。
  6. 代码导航:LSP 可以提供代码导航功能,以便快速定位和浏览代码。
  7. 代码分析:LSP 可以进行代码分析,以提供更准确的代码补全和代码重构建议。
  8. 代码片段:LSP 可以提供代码片段功能,以便快速插入常用代码模板。

LSP 可以大大提高编辑器和 IDE 的代码智能化和跨平台性,让开发者更加高效地编写和维护代码。

2. LSP 功能的代码示例:

1. 文档同步

import { createConnection, TextDocuments } from 'vscode-languageserver';

const connection = createConnection();
const documents = new TextDocuments();

documents.listen(connection);

documents.onDidChangeContent((change) => {
    console.log(change.document.uri, change.document.getText());
});

connection.listen();

上述代码中,我们创建了一个 documents 对象,并监听了 onDidChangeContent 事件。当文档发生变化时,事件处理函数会将文档的 URI 和文本内容打印出来。

2. 代码格式化

import { createConnection, TextDocuments } from 'vscode-languageserver';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { format } from 'prettier';

const connection = createConnection();
const documents = new TextDocuments(TextDocument);

documents.listen(connection);

connection.onRequest('textDocument/formatting', (params) => {
    const document = documents.get(params.textDocument.uri);
    const formatted = format(document.getText(), { parser: 'typescript' });
    return [{ range: { start: { line: 0, character: 0 }, end: document.positionAt(document.getText().length) }, newText: formatted }];
});

connection.listen();

上述代码中,我们使用了 prettier 模块来进行代码格式化。在 onRequest 事件处理函数中,我们监听了 textDocument/formatting 请求,并将请求的文本进行格式化,然后将格式化后的文本返回给客户端。

3. 语法检查

import { createConnection, TextDocuments, Diagnostic, DiagnosticSeverity } from 'vscode-languageserver';
import * as ts from 'typescript';

const connection = createConnection();
const documents = new TextDocuments();

documents.listen(connection);

connection.onDidChangeWatchedFiles((change) => {
    console.log('File change event received:', change);
});

connection.onDidChangeConfiguration((change) => {
    console.log('Configuration change event received:', change);
});

documents.onDidChangeContent((change) => {
    const diagnostics: Diagnostic[] = [];

    const program = ts.createProgram({
        rootNames: [change.document.uri],
        options: {},
    });

    const diagnosticsHost: ts.FormatDiagnosticsHost = {
        getCanonicalFileName: (path) => path,
        getCurrentDirectory: ts.sys.getCurrentDirectory,
        getNewLine: () => ts.sys.newLine,
    };

    const emitResult = program.emit();

    const allDiagnostics = ts
        .getPreEmitDiagnostics(program)
        .concat(emitResult.diagnostics)
        .filter((diagnostic) => diagnostic.file !== undefined);

    allDiagnostics.forEach((diagnostic) => {
        const { file } = diagnostic;
        if (file) {
            const { line, character } = file.getLineAndCharacterOfPosition(diagnostic.start!);
            const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
            const severity = diagnostic.category === ts.DiagnosticCategory.Error ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning;
            diagnostics.push({
                severity,
                range: {
                    start: { line, character },
                    end: { line, character: character + 1 },
                },
                message,
                source: 'typescript',
            });
        }
    });

    connection.sendDiagnostics({ uri: change.document.uri, diagnostics });
});

connection.listen();

上述代码中,我们使用了 TypeScript 编译器来进行语法检查。在 onDidChangeContent 事件处理函数中,我们使用 ts.createProgram 方法来创建一个 TypeScript 编译器实例,并使用 ts.getPreEmitDiagnostics 方法来获取所有的诊断信息。然后,我们将诊断信息转换成 LSP 的 Diagnostic 对象,并通过 connection.sendDiagnostics 方法将诊断信息发送给客户端。

4. 代码重构

import { createConnection, TextDocuments, Range, Position } from 'vscode-languageserver';
import * as ts from 'typescript';

const connection = createConnection();
const documents = new TextDocuments();

documents.listen(connection);

connection.onDidChangeWatchedFiles((change) => {
    console.log('File change event received:', change);
});

connection.onDidChangeConfiguration((change) => {
    console.log('Configuration change event received:', change);
});

documents.onDidChangeContent(async (change) => {
    const refactorInfo = await connection.sendRequest('textDocument/codeAction', {
        textDocument: change.document,
        range: Range.create(Position.create(0, 0), Position.create(change.document.lineCount, 0)),
        context: {
            diagnostics: [],
            only: ['refactor'],
        },
    });

    if (refactorInfo) {
        console.log(refactorInfo);
    }
});

connection.listen();

上述代码中,我们使用了 textDocument/codeAction 请求来进行代码重构。在 onDidChangeContent 事件处理函数中,我们向客户端发送了一条 textDocument/codeAction 请求,并将请求的范围设置为整个文档。然后,我们将请求的结果打印出来。

LSP 可以提供丰富的功能和事件,可以根据具体需求进行使用。