背景
笔者需要在本地搭建一个支持 python 代码进行补全,格式化,报错提示等功能的 web idea。 目前项目已经基于 monaco 完成了一个对 sql 代码进行处理的 idea,于是初步定下了桥接 pylsp 和 monaco 实现 python 编辑器的技术方案。
搭建 python-lsp-server ws 服务
目标:基于pylsp服务和ngxin,实现一个服务能够和 monaco 进行通信,完成基础功能
准备
python3.8.2 并编译好 pip。
网上很多 python 环境搭建的资料可供学习,此处不详表
下载 python-lsp-server 服务
pip3 install python-lsp-server /服务下载/ pip3 install python-lsp-server[all] /下载所需插件/
pylsp 下载成功后,执行 pylsp --version 正确输出 pylsp 版本即下载成功。
注:在格式化功能中 pylsp,black, python-lsp-black 三个版本号需要相互兼容 我使用的版本号是 pylsp v1.7.4 black 22.8.0 python-lsp-black 1.1.2
启动服务
pylsp 启动命令: pyslp --ws --host [your host] --port [your port] --log-file [your log file path] -v
低版本 pylsp 没有 --ws 命令,则需要搭建一个服务轮询和 pylsp 服务进行通信, 是否支持直接搭建 ws 服务可以通过 pylsp -h 查看输出结果中是否含有 --ws.
配置 pylsp.service
笔者服务器是 centos7 创建文件如下:
pylsp.service代码解读复制代码[Unit] Description=Python Language Server Protocol(pylsp) After=network.target [Service] Type=simple User=root ExecStart=[pylsp path] --ws --host [your host] --port [your port] --log-file [your log file path] -v Restart=on-failure RestartSec=5 [Install] wantedBy=multi-user.target
启动服务
systemctl daemon-reload // 使pylsp.service 生效
systemctl start pylsp.service // 启动 pylsp 服务
systemctl enable pylsp.service // 设置 pylsp 服务开机自启
systemctl statuus pylsp.service // 检查 pylsp 服务状态
返回结果中包含 active 即为启动成功
配置NGINX
笔者已经配置好了服务,目前是基于写好的配置上新增
nginx.conf代码解读复制代码server { listen 443 ssl; server_name [your server]; /* 非空,是已经写好的转发配置 */ location /ws { proxy_pass `http://${your host}:${your port}`; proxy_http_version 1.1; // 这里不能掉,默认的访问会被 ws 服务拒绝 proxy_set_header Upgrade "upgrade"; proxy_set_header Connection "upgrade" } }
monaco web 端对接 pylsp 服务
创建 socket 服务,开始和 pylsp 服务通信
initLspServer.js代码解读复制代码// 如果是直接通过 http 通信则有 WS_ENDPOINT = `ws://[yourhost]:[your port]` 即可 const WS_ENDPOINT = `wws://[your server]/ws`; const socket = new WebSocket(WS_ENDPOINT); const pendingPromises = new Map(); // 通过这个 map 包裹待响应的socket请求,用于后续直接链式调用 socket 请求 const rootUri = "mem:///" // web idea 可供pylsp 服务访问的文件实例,则通过这个路径告知服务,是虚拟路径 const requestId = 1; const useId = getUSerId(); // 编辑器支持多人协同编辑,则请求id 和 userId 关联, 避免并发请求出错 const sendRequesPromise = (request, timeout = 5000) => new Promise((resolve, reject) => { const {id} = request; pendingPromises.set(`${userId}_${id}`, {resolve, reject}); socket.send(JSON.stringify(request)); }); // 处理 LSP 服务响应 socket.onmessage = (event) => { const response = JSON.parse(event.data); const {id} = response; const socketId = `${userId}_{id}`; if(pendingPromises.has(socketId)) { const {resolve, reject} = pendingPromises.get(socketId); response.error ? reject(response.error) : resolve(response); pendingPromises.delete(socketId); } if(resopnse.method === 'textDocument/publishDiagnostics') setDiagnosticsMarkets(response.params.diagnostics); }; // 初始化 LSP 服务 const initLSPServer = () => new Promise(resolve) => { const initRequest = { jsonrpc: "2.0", id: requestId, method: "iniialize", params: { procesId: requestId, rootUri, capabilities: { textDocument: { synchronization: { dynamicRegistration: true }, completion: { completionItem: { snippetSupport: true } }, formatting: { dynamicRegistration: true } } } } }; sendRequestPromise(initRequest).then(res => resolve(res)); }; // 告知 LSP, 某.py文件已打开 const initOpenFile = () => { const text = editor.getValue(); // editor 即为 monaco 编辑器实例 const fileName = getFileName(); // file Name 即为文件名称,涉及到协同编辑时,则fileName 拼上userId const didOpenMsg = { jsonrpc: "2.0", method: "textDocument/didOpen", params: { textDocument: { languageId: "python", version: 1, text: text, uri: `${rootUri}${fileName}` // 后续执行补全,报错诊断,格式化 lsp服务都需要依赖此Id获取文件实例 } } } }; socket.onopen = () => { initLSPServer.then(() => initOpenFile()); }
onEditorChange.js代码解读复制代码// 将此函数绑定到 monaco 的 change 事件上 const onEditorChange = () => { const text = editor.getValue(); const textDocument = { uri: fileUri, id: requsetId, languageId: "python", version: 1, text: text }; const didChangeMsg = { jsonrpc: "2.0", method: "textDocument/didChange", params: { textDocument: textDocument, contentChanges: [{text: text}] } }; // 告知lsp服务,文件内容变动 socket.send(JSON.strigify(didChangeMsg)) };
getDiagnostics.js代码解读复制代码// pylsp 发起获取报错信息的请求,返回信息中没有具体的报错信息,因此通过 socketId 拿到返回信息添加报错装饰不可取 const getDiagnostics = () => { const text = editor.getValue(); const textDocument = { uri: fileUri, id: requsetId, languageId: "python", version: 1, text: text }; const diagnosticsMsg = { id: requestId, jsonrpc: "2.0", method: "textDocument/documentSymbol", params: { textDocument: textDocument } }; sendRequestPromise(diagnosticsMsg); }; const setDiagnosticsMarkets = (diagnostics) => { if(!diagnostics || diagnostics.length === 0) { const markers = diagnostics.map(d => ({ message: d.message, severity: monaco.MarkerSeverity.Error, // 根据 d 返回可以分级处理报错信息 startLineNumber: d.range.start.line + 1, endLineNumber: d.range.end.line + 1, startColumn: d.range.start.character + 1, endColumn: d.range.end.character + 1 })); monaco.edditor.setMoedlMarkers(editor.getModel, 'python', markers); } };
评论记录:
回复评论: