PYLSP 桥接 MONACO

背景

笔者需要在本地搭建一个支持 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);
  }
};
相关推荐
冬阳春晖23 分钟前
web animation API 锋利的css动画控制器 (更新中)
前端·javascript·css
Python私教2 小时前
使用FastAPI和React以及MongoDB构建全栈Web应用05 FastAPI快速入门
前端·react.js·fastapi
浪裡遊2 小时前
Typescript中的对象类型
开发语言·前端·javascript·vue.js·typescript·ecmascript
杨-羊羊羊2 小时前
什么是深拷贝什么是浅拷贝,两者区别
开发语言·前端·javascript
发呆的薇薇°2 小时前
在vue里,使用dayjs格式化时间并实现日期时间的实时更新
前端·javascript·vue.js
七冬与小糖2 小时前
【本地搭建npm私服】使用Verdaccio
前端·npm·node.js
lally.2 小时前
2025御网杯wp(web,misc,crypto)
前端·ctf
海绵不是宝宝8172 小时前
React+Springboot项目部署ESC服务器
前端·react.js·前端框架
前端小崔3 小时前
从零开始学习three.js(15):一文详解three.js中的纹理映射UV
前端·javascript·学习·3d·webgl·数据可视化·uv
ZHOU_WUYI3 小时前
React 实现 JWT 登录验证的最小可运行示例
前端·react.js·前端框架