[Feature Request] 支持多人编辑吗?
Prerequisites
- [x] There isn't an existing issue that requests the same feature, to avoid duplicates.
Clear and concise description of the problem
支持多人编辑吗?
Suggested solution
No response
Further Information
No response
Contributing
None
https://github.com/user-attachments/assets/7285985a-37f9-4c8c-a77f-f08bd1dd90a6
理论上Cherry已经准备好支持多人编辑了(但需要先把 #1405 支持上),通过 setMarkdown(content:string, keepCursor = false) 可以实现把服务端的内容更新到本地时不中断本地用户的编辑操作。大概流程如下:
有一些功能需要业务方自行实现:
- 第7步 和 第12步 解决冲突的逻辑,比较典型的算法就是OT算法
- 在客户端回显其他用户光标位置的交互(可以是回显带颜色、头像、用户信息的光标,也有可能是高亮所在段落,看业务的设计规范)
- 如果业务方通过websocket来实现,还需要额外实现实时保存(大概率在第7步实现下就好)、自动生成版本号、断网重连、弱网络优化等功能。
_20250825_124626.mp4 理论上Cherry已经准备好支持多人编辑了(但需要先把 #1405 支持上),通过
setMarkdown(content:string, keepCursor = false)可以实现把服务端的内容更新到本地时不中断本地用户的编辑操作。大概流程如下:有一些功能需要业务方自行实现:
- 第7步 和 第12步 解决冲突的逻辑,比较典型的算法就是OT算法
- 在客户端回显其他用户光标位置的交互(可以是回显带颜色、头像、用户信息的光标,也有可能是高亮所在段落,看业务的设计规范)
- 如果业务方通过websocket来实现,还需要额外实现实时保存(大概率在第7步实现下就好)、自动生成版本号、断网重连、弱网络优化等功能。
在收到websocket推送的时候,老是光标位置不对,请问要怎么处理
有可能是没做第12步导致本地更新服务器版本内容时产生了回档,另外更新内容用的cherryObj.setMarkdown() api有传第二个参数么?
有可能是没做第12步导致本地更新服务器版本内容时产生了回档,另外更新内容用的cherryObj.setMarkdown() api有传第二个参数么? 传了,就是光标老是跳来跳去的
那光标跳来跳去的时候,文档内容有没有出现回档之类的情况? Cherry保持光标的做法是把旧内容和新内容做diff,根据diff计算光标更新后的位置(具体代码在这里),有可能是diff逻辑有问题,但现在提供的信息有限不太好判断。。。
那光标跳来跳去的时候,文档内容有没有出现回档之类的情况? Cherry保持光标的做法是把旧内容和新内容做diff,根据diff计算光标更新后的位置(具体代码在这里),有可能是diff逻辑有问题,但现在提供的信息有限不太好判断。。。
并没有出现回档的问题,就是A在编辑的时候,导致B的鼠标一直在跳动
竟然是这么个情况。。我们重现定位下哈
https://github.com/user-attachments/assets/64078a2c-e2b3-4b65-96f4-c6f4f9523773
额,没能复现出来,想问下你们出现问题的内容多不多,大概有哪些内容,或者可以在下面的地址重现出来不? https://tencent.github.io/cherry-markdown/examples/api.html 测试的代码大概这样:
cherryObj.setMarkdown("输入内容");
setTimeout(()=>{cherryObj.setMarkdown("输11111111111内容3223",1);},5000);
setTimeout(()=>{cherryObj.setMarkdown("输2211111111111内容3223",1);},6000);
setTimeout(()=>{cherryObj.setMarkdown("输221111113311111内容3223",1);},7000);
setTimeout(()=>{cherryObj.setMarkdown("输221111113311111内55容3223",1);},8000);
setTimeout(()=>{cherryObj.setMarkdown("输1111111内容3223",1);},9000);
setTimeout(()=>{cherryObj.setMarkdown("输入内容",1);},9000);
setTimeout
您在setTimeout的时候,继续输入信息,你看看光标位置会不会发生改变
https://github.com/user-attachments/assets/2b9e7dab-9ab1-4eb8-a34c-0103bdd75302
额,还是不行哦。。。测试代码如下:
cherryObj.setMarkdown("输入内容");
// 在字符串随机位置插入字符
function insertStr(soure, index, str) {
return soure.slice(0, index) + str + soure.slice(index);
}
setTimeout(()=>{cherryObj.setMarkdown(insertStr(cherryObj.getValue(), Math.floor(Math.random() * cherryObj.getValue().length), " hello "),1);},5000);
setTimeout(()=>{cherryObj.setMarkdown(insertStr(cherryObj.getValue(), Math.floor(Math.random() * cherryObj.getValue().length), " `world` "),1);},6000);
setTimeout(()=>{cherryObj.setMarkdown(insertStr(cherryObj.getValue(), Math.floor(Math.random() * cherryObj.getValue().length), " 测试 "),1);},7000);
setTimeout(()=>{cherryObj.setMarkdown(insertStr(cherryObj.getValue(), Math.floor(Math.random() * cherryObj.getValue().length), " \n# 测试\n "),1);},8000);
_20250827_095312.mp4 额,还是不行哦。。。测试代码如下:
cherryObj.setMarkdown("输入内容"); // 在字符串随机位置插入字符 function insertStr(soure, index, str) { return soure.slice(0, index) + str + soure.slice(index); } setTimeout(()=>{cherryObj.setMarkdown(insertStr(cherryObj.getValue(), Math.floor(Math.random() * cherryObj.getValue().length), " hello "),1);},5000); setTimeout(()=>{cherryObj.setMarkdown(insertStr(cherryObj.getValue(), Math.floor(Math.random() * cherryObj.getValue().length), " `world` "),1);},6000); setTimeout(()=>{cherryObj.setMarkdown(insertStr(cherryObj.getValue(), Math.floor(Math.random() * cherryObj.getValue().length), " 测试 "),1);},7000); setTimeout(()=>{cherryObj.setMarkdown(insertStr(cherryObj.getValue(), Math.floor(Math.random() * cherryObj.getValue().length), " \n# 测试\n "),1);},8000);
有没有什么办法获取到新输入的内容
_20250827_095312.mp4 额,还是不行哦。。。测试代码如下:
cherryObj.setMarkdown("输入内容"); // 在字符串随机位置插入字符 function insertStr(soure, index, str) { return soure.slice(0, index) + str + soure.slice(index); } setTimeout(()=>{cherryObj.setMarkdown(insertStr(cherryObj.getValue(), Math.floor(Math.random() * cherryObj.getValue().length), " hello "),1);},5000); setTimeout(()=>{cherryObj.setMarkdown(insertStr(cherryObj.getValue(), Math.floor(Math.random() * cherryObj.getValue().length), " `world` "),1);},6000); setTimeout(()=>{cherryObj.setMarkdown(insertStr(cherryObj.getValue(), Math.floor(Math.random() * cherryObj.getValue().length), " 测试 "),1);},7000); setTimeout(()=>{cherryObj.setMarkdown(insertStr(cherryObj.getValue(), Math.floor(Math.random() * cherryObj.getValue().length), " \n# 测试\n "),1);},8000);有没有什么办法获取到新输入的内容
https://github.com/user-attachments/assets/49181034-0a9c-4173-b716-b1920bb6c3e9 现在就是A正常,B的话,就会无限新增

额,这不是“获取新输入的内容”的问题,是需要利用OT算法根据三个版本的内容拿到最终变更结果的问题,这三个版本内容分别为:V1 上一次服务器下发的版本;V2 本地文档最新内容;V3 当前服务器下发的版本。 通过 diff1 = V2 - V1; diff2 = V3 - V1; diffTarget = merge(diff1, diff2),获取最终版本 VTarget = V2.patch(diffTarget),然后 通过 cherry.setMarkdown(VTarget , 1) 完成更新。这一步就是上面流程图里的第12步和第13步的工作。 具体的如何获得diff,如何解决diff冲突,如何将最终diff更新到文档里,基本都是OT算法的核心能力了,可以了解下OT算法并应用哈。 另外需要注意,第7步服务端也要通过OT算法解决冲突哈。
额,这不是“获取新输入的内容”的问题,是需要利用OT算法根据三个版本的内容拿到最终变更结果的问题,这三个版本内容分别为:V1 上一次服务器下发的版本;V2 本地文档最新内容;V3 当前服务器下发的版本。 通过 diff1 = V2 - V1; diff2 = V3 - V1; diffTarget = merge(diff1, diff2),获取最终版本 VTarget = V2.patch(diffTarget),然后 通过 cherry.setMarkdown(VTarget , 1) 完成更新。这一步就是上面流程图里的第12步和第13步的工作。 具体的如何获得diff,如何解决diff冲突,如何将最终diff更新到文档里,基本都是OT算法的核心能力了,可以了解下OT算法并应用哈。 另外需要注意,第7步服务端也要通过OT算法解决冲突哈。
第7步 第12步,13步,有没有demo可以看一下
https://github.com/yjs/yjs 可以参考这个哈(这个并非单纯的OT算法的实现,而是有自己的一套机制,但似乎据说性能反而更高)
https://github.com/yjs/yjs 可以参考这个哈(这个并非单纯的OT算法的实现,而是有自己的一套机制,但似乎据说性能反而更高)
这样光标还是会跳动
确定这个content是符合预期的吗?
撤销有最大限制吗?
https://github.com/user-attachments/assets/3abc948a-20f4-44e6-b1ff-2a7d686b3664
我用谷歌浏览器同时打开了两个markdown,A切到B的时候,setMarkdown一次内容,B在切到A的时候,也进行了同样的操作,但是在切到A的时候,光标的位置始终都会发生改变,而且都是往后移动了2-3个字,有时候会更多
视频里的信息太少,看不出来问题,建议先确认每次更新时的内容是否符合预期哈
https://github.com/user-attachments/assets/5e50efc6-3914-42b9-82fe-a28be0c8736b
这边试了下是ok的。。。
_20250829_185247.mp4 这边试了下是ok的。。。
如果是OT算法的话,我是把全文本给后端,还是说只给我新输入的内容给后端, 后端是把整合后的全文本给我,我再setmarkdown吗,如果不是全文本给我,给我位置让我插入的画,我要用哪个方法?
有一些功能需要业务方自行实现:
确定这个content是符合预期的吗?