背景
笔者需要在本地搭建一个支持 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);
}
};