乐趣区

关于人工智能:如何创建集成-LSP-支持多语言的-Web-代码编辑器

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

目前有着多款 web 上的代码编辑器可供选择,比方 AceCodeMirrorMonaco,这三款编辑器的比拟在这篇文章中有着具体的介绍,在此就不作过多赘述。这篇文章咱们抉择 Monaco Editor 来对 LSP 进行集成,从而在实践上可能反对所有的编程语言。

原文链接:https://forum.laf.run/d/1027

什么是 LSP

LSP(Language Server Protocol),也就是语言服务协定,更具体更艰深地说就是定义了在代码编辑器和语言服务器之间的一套标准,从而让本来

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

变为

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

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

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

预览

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

Server 端开发

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

这里咱们抉择微软官网保护的 pyright 提供语言服务

首先创立 Express 服务器,配置动态文件服务,应用 fileURLToPathdirname 来获取以后文件的门路,并将服务设置在 30000 端口

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。

const wss = new WebSocketServer({noServer: true,});

而这里咱们须要创立本人的 HTTP 服务器,并手动解决浏览器的 upgrade 申请。上面代码便是如何监听 upgrade 事件并进行解决。

server.on('upgrade',()=>{});

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

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

const baseDir = resolve(getLocalDirectory(import.meta.url));
const relativeDir = '../../../node_modules/pyright/dist/pyright-langserver.js';
const ls = resolve(baseDir, relativeDir); 

再创立语言服务器的连贯 和 创立 WebSocket 的数据连贯

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,如下:

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 的语言服务与主题显示。

await initServices({
    enableModelService: true,
    enableThemeService: true,
    enableTextmateService: true,
    enableLanguagesService: true,
})

而后创立可能与语言服务器相连的 WebSocket 连贯。

createWebSocket("ws://localhost:30000/pyright");

而创立这个连贯也须要用到 monaco-languageclient。

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 实例的输入输出。

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 实例即可,还能进行些许的配置。

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',
});

就这样咱们也实现了前端。

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 这类优良的开源我的项目提供反对,可能让咱们在仅仅拼凑了几段代码后领有不错的代码编辑器成果。下一步打算用 LSP 革新 Laf 的 Web IDE。

因为我也只是刚刚接触这块常识,文章中不免有错漏,心愿能与读到这里的各位独特交换提高。

参考资料

  • https://github.com/microsoft/language-server-protocol/wiki/Protocol-History
  • https://medium.com/@malintha1996/understanding-the-language-s…
  • https://ubug.io/blog/workpad-part-6
  • https://www.typefox.io/blog/how-to-embed-a-monaco-editor-in-a…
  • https://www.typefox.io/blog/teaching-the-language-server-prot…
退出移动版