如何创建集成 LSP 的 Web 代码编辑器(支持多语言)

对于一个云开发平台来说,一个好的 Web IDE 能很大程度地提高用户的编码体验,而一个 Web IDE 的一个重要组成部分就是代码编辑器。

目前有着多款 web 上的代码编辑器可供选择,比如 Ace、CodeMirror、Monaco,这三款编辑器的比较在这篇文章中有着详细的介绍,在此就不作过多赘述。这篇文章我们选择 Monaco Editor 来对 LSP 进行集成,从而在理论上能够支持所有的编程语言。

什么是 LSP

LSP(Language Server Protocol),也就是语言服务协议,更具体更通俗地说就是定义了在代码编辑器和语言服务器之间的一套规范,从而让原本

m 个编辑器与 n 个编程语言之间的对应关系

变为

m 个编辑器与 LSP 的关系和 n 个编程语言与 LSP 之间的关系,

从而将开发的复杂度由 m*n 降到了 m+n。

除了对编辑器开发者和编程语言开发者友好,对我们这种尝试让一个编辑器支持多种语言的开发者也更是友好,有 vscode 这样的编辑器珠玉在前,便能轻松地根据 vscode 的设计思路实现我们的需求。

LAF预览

在这篇文章中,我们会开发一个最小最轻量的编辑器 Demo 作为演示,架构非常简单,就是前端创建一个 Monaco Editor,后端创建一个语言服务器,二者之间通过 vscode-ws-jsonrpc 和 WebSocket 服务进行传输,实际实现的 Web 端 Python 编辑器如下:

Server 端开发

在 Web 端能接入语言服务前,我们得先在服务端运行一个语言服务,langserver.org/ 这个网站收录了许多语言服务的实现,

这里我们选择微软官方维护的 pyright 提供语言服务

首先创建 Express 服务器,配置静态文件服务,使用 fileURLToPath 和 dirname 来获取当前文件的路径,并将服务设置在 30000 端口

ini 复制代码
const app = express();
const __filename = fileURLToPath(import.meta.url);
const dir = dirname(__filename);
app.use(express.static(dir));
const server = app.listen(30000);

然后我们需要创建一个 WebSocket Server,注意这里的 noServer 参数,如果没有指定 noServer,那么 WebSocketServer 会自动创建一个 http server 来处理浏览器的 HTTP 请求到 WebSocket 请求的 upgrade。

arduino 复制代码
const wss = new WebSocketServer({
 noServer: true,
});

而这里我们需要创建自己的 HTTP 服务器,并手动处理浏览器的 upgrade 请求。下面代码便是如何监听 upgrade 事件并进行处理。

dart 复制代码
server.on('upgrade',()=>{});

在处理函数中,按照下面的代码将 WebSocket 使用到 jsonrpc 协议中,并启动语言服务器让二者相连。

先构建语言服务器的路径,找到 pyright 包所在的位置。

const 复制代码
const relativeDir = '../../../node_modules/pyright/dist/pyright-langserver.js';
const ls = resolve(baseDir, relativeDir); 

再创建语言服务器的连接 和 创建 WebSocket 的数据连接

ini 复制代码
const serverConnection = createServerProcess(serverName, 'node', [ls, '--stdio']);
const reader = new WebSocketMessageReader(socket);
const writer = new WebSocketMessageWriter(socket);
const socketConnection = createConnection(reader, writer, () => socket.dispose());

最后用 forward 函数将消息从 soketConnection 转发到 serverConnection,如下:

ini 复制代码
forward(socketConnection, serverConnection, message => {
    if (Message.isRequest(message)) {
        console.log(`Received:`);
        console.log(message);
 }
    if (Message.isResponse(message)) {
        console.log(`Sent:`);
        console.log(message);
    }
 return message;
});

于是我们将语言服务器跑起来,并在文章后面阶段会写的前端编辑器中随便输入一点东西,可以看到终端里输出了 message,

这样我们就使用 pyright 完成了语言服务器的开发。

Web 端开发

接下来我们开发前端的内容,还是在上面的那个网站中,

可以看到 Monaco Editor 也是有支持 LSP 的方案的,所以我们使用 TypeFox 开发的 monaco-languageclient 对monaco 进行集成 lsp 的开发。

首先使用 monaco-languageclient 的 initServices 函数初始化一些服务,其中最重要的就是下面四个配置,定义了 Monaco 的语言服务与主题显示。

php 复制代码
await initServices({
    enableModelService: true,
    enableThemeService: true,
    enableTextmateService: true,
    enableLanguagesService: true,
})

然后创建能够与语言服务器相连的 WebSocket 连接。

scss 复制代码
createWebSocket("ws://localhost:30000/pyright");

而创建这个连接也需要用到 monaco-languageclient。

ini 复制代码
const createWebSocket = (url: string): WebSocket => {
    const webSocket = new WebSocket(url);
    webSocket.onopen = async () => {
        const socket = toSocket(webSocket);
        const reader = new WebSocketMessageReader(socket);
        const writer = new WebSocketMessageWriter(socket);
        languageClient = createLanguageClient({
            reader,
            writer
        });
        await languageClient.start();
        reader.onClose(() => languageClient.stop());
    };
    return webSocket;
};

const createLanguageClient = (transports: MessageTransports): MonacoLanguageClient => {
    return new MonacoLanguageClient({
        name: 'Pyright Language Client',
        clientOptions: {
            documentSelector: [languageId],
            errorHandler: {
                error: () => ({ action: ErrorAction.Continue }),
                closed: () => ({ action: CloseAction.DoNotRestart })
            },
            workspaceFolder: {
                index: 0,
                name: 'workspace',
                uri: monaco.Uri.parse('/tmp')
            },
            synchronize: {
                fileEvents: [vscode.workspace.createFileSystemWatcher('**')]
            }
        },
        connectionProvider: {
            get: () => {
                return Promise.resolve(transports);
            }
        }
    });
};

接下来这里需要创建一个虚拟文件系统,作为 Monaco Editor 实例的输入输出。

dart 复制代码
const fileSystemProvider = new RegisteredFileSystemProvider(false);
fileSystemProvider.registerFile(new RegisteredMemoryFile(vscode.Uri.file('/test.py'), 'print("Hello, laf!")'));
registerFileSystemOverlay(1, fileSystemProvider);
const modelRef = await createModelReference(monaco.Uri.file('/test.py'));

最后创建 Monaco Editor 实例即可,还能进行些许的配置。

php 复制代码
createConfiguredEditor(document.getElementById('container')!, {
    model: modelRef.object.textEditorModel,
    automaticLayout: true,
    minimap: {
     enabled: false
    },
    scrollbar: {
        verticalScrollbarSize: 4,
        horizontalScrollbarSize: 8,
    },
    overviewRulerLanes: 0,
    lineNumbersMinChars: 4,
    scrollBeyondLastLine: false,
    theme: 'vs',
});

就这样我们也完成了前端。

javascript 复制代码
import Editor from './python/Editor';

function App() {
  
  return (
    <>
      <h2>monaco python lsp</h2>
      <div style={{height:"500px", width:"800px", border:"1px solid black", padding:"8px 0"}}>
        <Editor />
      </div>
    </>
  )
}

export default App

效果如下

小结

要深入理解 LSP 以及其背后的工作原理还是有很大的难度的,但是好在有 languageserver,languageclient 这类优秀的开源项目提供支持,能够让我们在仅仅拼凑了几段代码后拥有不错的代码编辑器效果。

由于我也只是刚刚接触这块知识,文章中难免有错漏,希望能与读到这里的各位共同交流进步。

参考资料

github.com/microsoft/l...

medium.com/@malintha19...

ubug.io/blog/workpa...

www.typefox.io/blog/how-to...

www.typefox.io/blog/teachi...

🏠官网链接 laf.run

🐙GitHub 地址 github.com/labring/laf

📑访问 Laf 文档 doc.laf.run/guide/

🏘️逛逛论坛 forum.laf.run/

关于 Laf Laf 是一款为所有开发者打造的集函数、数据库、存储为一体的云开发平台,助你像写博客一样写代码,随时随地发布上线应用!3 分钟上线 ChatGPT 应用!

sealos 以kubernetes为内核的云操作系统发行版,让云原生简单普及

laf 写代码像写博客一样简单,什么docker kubernetes统统不关心,我只关心写业务!

相关推荐
Grassto1 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto3 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
程序设计实验室4 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题4 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo
啊汉6 天前
古文观芷App搜索方案深度解析:打造极致性能的古文搜索引擎
go·软件随想
asaotomo6 天前
一款 AI 驱动的新一代安全运维代理 —— DeepSentry(深哨)
运维·人工智能·安全·ai·go
码界奇点7 天前
基于Gin与GORM的若依后台管理系统设计与实现
论文阅读·go·毕业设计·gin·源代码管理
迷迭香与樱花7 天前
Gin 框架
go·gin
只是懒得想了7 天前
用Go通道实现并发安全队列:从基础到最佳实践
开发语言·数据库·golang·go·并发安全
fenglllle8 天前
使用fyne做一个桌面ipv4网段计算程序
开发语言·go