如何创建集成 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统统不关心,我只关心写业务!

相关推荐
梁梁梁梁较瘦1 天前
边界检查消除(BCE,Bound Check Elimination)
go
梁梁梁梁较瘦1 天前
指针
go
梁梁梁梁较瘦1 天前
内存申请
go
半枫荷1 天前
七、Go语法基础(数组和切片)
go
梁梁梁梁较瘦2 天前
Go工具链
go
半枫荷2 天前
六、Go语法基础(条件控制和循环控制)
go
半枫荷3 天前
五、Go语法基础(输入和输出)
go
小王在努力看博客3 天前
CMS配合闲时同步队列,这……
go
Anthony_49264 天前
逻辑清晰地梳理Golang Context
后端·go
Dobby_055 天前
【Go】C++ 转 Go 第(二)天:变量、常量、函数与init函数
vscode·golang·go