cherry-markdown icon indicating copy to clipboard operation
cherry-markdown copied to clipboard

[Feature Request] 支持多人编辑吗?

Open meiluowuchen opened this issue 4 months ago • 22 comments

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

meiluowuchen avatar Aug 23 '25 06:08 meiluowuchen

https://github.com/user-attachments/assets/7285985a-37f9-4c8c-a77f-f08bd1dd90a6

理论上Cherry已经准备好支持多人编辑了(但需要先把 #1405 支持上),通过 setMarkdown(content:string, keepCursor = false) 可以实现把服务端的内容更新到本地时不中断本地用户的编辑操作。大概流程如下:

Image

有一些功能需要业务方自行实现:

  1. 第7步第12步 解决冲突的逻辑,比较典型的算法就是OT算法
  2. 在客户端回显其他用户光标位置的交互(可以是回显带颜色、头像、用户信息的光标,也有可能是高亮所在段落,看业务的设计规范)
  3. 如果业务方通过websocket来实现,还需要额外实现实时保存(大概率在第7步实现下就好)、自动生成版本号、断网重连、弱网络优化等功能。

sunsonliu avatar Aug 25 '25 05:08 sunsonliu

_20250825_124626.mp4 理论上Cherry已经准备好支持多人编辑了(但需要先把 #1405 支持上),通过 setMarkdown(content:string, keepCursor = false) 可以实现把服务端的内容更新到本地时不中断本地用户的编辑操作。大概流程如下:

Image 有一些功能需要业务方自行实现:
  1. 第7步第12步 解决冲突的逻辑,比较典型的算法就是OT算法
  2. 在客户端回显其他用户光标位置的交互(可以是回显带颜色、头像、用户信息的光标,也有可能是高亮所在段落,看业务的设计规范)
  3. 如果业务方通过websocket来实现,还需要额外实现实时保存(大概率在第7步实现下就好)、自动生成版本号、断网重连、弱网络优化等功能。

在收到websocket推送的时候,老是光标位置不对,请问要怎么处理

meiluowuchen avatar Aug 27 '25 01:08 meiluowuchen

有可能是没做第12步导致本地更新服务器版本内容时产生了回档,另外更新内容用的cherryObj.setMarkdown() api有传第二个参数么?

sunsonliu avatar Aug 27 '25 01:08 sunsonliu

有可能是没做第12步导致本地更新服务器版本内容时产生了回档,另外更新内容用的cherryObj.setMarkdown() api有传第二个参数么? 传了,就是光标老是跳来跳去的

meiluowuchen avatar Aug 27 '25 01:08 meiluowuchen

那光标跳来跳去的时候,文档内容有没有出现回档之类的情况? Cherry保持光标的做法是把旧内容和新内容做diff,根据diff计算光标更新后的位置(具体代码在这里),有可能是diff逻辑有问题,但现在提供的信息有限不太好判断。。。

sunsonliu avatar Aug 27 '25 01:08 sunsonliu

那光标跳来跳去的时候,文档内容有没有出现回档之类的情况? Cherry保持光标的做法是把旧内容和新内容做diff,根据diff计算光标更新后的位置(具体代码在这里),有可能是diff逻辑有问题,但现在提供的信息有限不太好判断。。。

并没有出现回档的问题,就是A在编辑的时候,导致B的鼠标一直在跳动

meiluowuchen avatar Aug 27 '25 01:08 meiluowuchen

竟然是这么个情况。。我们重现定位下哈

sunsonliu avatar Aug 27 '25 01:08 sunsonliu

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);

sunsonliu avatar Aug 27 '25 01:08 sunsonliu

setTimeout

您在setTimeout的时候,继续输入信息,你看看光标位置会不会发生改变

meiluowuchen avatar Aug 27 '25 01:08 meiluowuchen

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);

sunsonliu avatar Aug 27 '25 01:08 sunsonliu

_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);

有没有什么办法获取到新输入的内容

meiluowuchen avatar Aug 27 '25 02:08 meiluowuchen

_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的话,就会无限新增

meiluowuchen avatar Aug 27 '25 02:08 meiluowuchen

img

额,这不是“获取新输入的内容”的问题,是需要利用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算法解决冲突哈。

sunsonliu avatar Aug 27 '25 03:08 sunsonliu

img

额,这不是“获取新输入的内容”的问题,是需要利用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可以看一下

meiluowuchen avatar Aug 27 '25 05:08 meiluowuchen

https://github.com/yjs/yjs 可以参考这个哈(这个并非单纯的OT算法的实现,而是有自己的一套机制,但似乎据说性能反而更高)

sunsonliu avatar Aug 27 '25 06:08 sunsonliu

https://github.com/yjs/yjs 可以参考这个哈(这个并非单纯的OT算法的实现,而是有自己的一套机制,但似乎据说性能反而更高)

这样光标还是会跳动

meiluowuchen avatar Aug 27 '25 08:08 meiluowuchen

Image 确定这个content是符合预期的吗?

sunsonliu avatar Aug 27 '25 10:08 sunsonliu

Image 确定这个content是符合预期的吗?

撤销有最大限制吗?

meiluowuchen avatar Aug 28 '25 10:08 meiluowuchen

https://github.com/user-attachments/assets/3abc948a-20f4-44e6-b1ff-2a7d686b3664

我用谷歌浏览器同时打开了两个markdown,A切到B的时候,setMarkdown一次内容,B在切到A的时候,也进行了同样的操作,但是在切到A的时候,光标的位置始终都会发生改变,而且都是往后移动了2-3个字,有时候会更多

meiluowuchen avatar Aug 29 '25 07:08 meiluowuchen

视频里的信息太少,看不出来问题,建议先确认每次更新时的内容是否符合预期哈

sunsonliu avatar Aug 29 '25 10:08 sunsonliu

https://github.com/user-attachments/assets/5e50efc6-3914-42b9-82fe-a28be0c8736b

这边试了下是ok的。。。

sunsonliu avatar Aug 29 '25 10:08 sunsonliu

_20250829_185247.mp4 这边试了下是ok的。。。

如果是OT算法的话,我是把全文本给后端,还是说只给我新输入的内容给后端, 后端是把整合后的全文本给我,我再setmarkdown吗,如果不是全文本给我,给我位置让我插入的画,我要用哪个方法?

meiluowuchen avatar Aug 30 '25 07:08 meiluowuchen